LSST Applications 27.0.0,g0265f82a02+469cd937ee,g02d81e74bb+21ad69e7e1,g1470d8bcf6+cbe83ee85a,g2079a07aa2+e67c6346a6,g212a7c68fe+04a9158687,g2305ad1205+94392ce272,g295015adf3+81dd352a9d,g2bbee38e9b+469cd937ee,g337abbeb29+469cd937ee,g3939d97d7f+72a9f7b576,g487adcacf7+71499e7cba,g50ff169b8f+5929b3527e,g52b1c1532d+a6fc98d2e7,g591dd9f2cf+df404f777f,g5a732f18d5+be83d3ecdb,g64a986408d+21ad69e7e1,g858d7b2824+21ad69e7e1,g8a8a8dda67+a6fc98d2e7,g99cad8db69+f62e5b0af5,g9ddcbc5298+d4bad12328,ga1e77700b3+9c366c4306,ga8c6da7877+71e4819109,gb0e22166c9+25ba2f69a1,gb6a65358fc+469cd937ee,gbb8dafda3b+69d3c0e320,gc07e1c2157+a98bf949bb,gc120e1dc64+615ec43309,gc28159a63d+469cd937ee,gcf0d15dbbd+72a9f7b576,gdaeeff99f8+a38ce5ea23,ge6526c86ff+3a7c1ac5f1,ge79ae78c31+469cd937ee,gee10cc3b42+a6fc98d2e7,gf1cff7945b+21ad69e7e1,gfbcc870c63+9a11dc8c8f
LSST Data Management Base Package
Loading...
Searching...
No Matches
dipoleMeasurement.py
Go to the documentation of this file.
1# This file is part of ip_diffim.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21
22import numpy as np
23
24import lsst.afw.image as afwImage
25import lsst.geom as geom
26import lsst.pex.config as pexConfig
27import lsst.meas.deblender.baseline as deblendBaseline
28from lsst.meas.base.pluginRegistry import register
29from lsst.meas.base import SingleFrameMeasurementTask, SingleFrameMeasurementConfig, \
30 SingleFramePluginConfig, SingleFramePlugin
31import lsst.afw.display as afwDisplay
32from lsst.utils.logging import getLogger
33
34__all__ = ("DipoleMeasurementConfig", "DipoleMeasurementTask", "DipoleAnalysis", "DipoleDeblender",
35 "SourceFlagChecker", "ClassificationDipoleConfig", "ClassificationDipolePlugin")
36
37
39 """Configuration for classification of detected diaSources as dipole or not"""
40 minSn = pexConfig.Field(
41 doc="Minimum quadrature sum of positive+negative lobe S/N to be considered a dipole",
42 dtype=float, default=np.sqrt(2) * 5.0,
43 )
44 maxFluxRatio = pexConfig.Field(
45 doc="Maximum flux ratio in either lobe to be considered a dipole",
46 dtype=float, default=0.65
47 )
48
49
50@register("ip_diffim_ClassificationDipole")
52 """A plugin to classify whether a diaSource is a dipole.
53 """
54
55 ConfigClass = ClassificationDipoleConfig
56
57 @classmethod
59 """
60 Returns
61 -------
62 result : `callable`
63 """
64 return cls.APCORR_ORDER
65
66 def __init__(self, config, name, schema, metadata):
67 SingleFramePlugin.__init__(self, config, name, schema, metadata)
69 self.keyProbability = schema.addField(name + "_value", type="D",
70 doc="Set to 1 for dipoles, else 0.")
71 self.keyFlag = schema.addField(name + "_flag", type="Flag", doc="Set to 1 for any fatal failure.")
72
73 def measure(self, measRecord, exposure):
74 passesSn = self.dipoleAnalysis.getSn(measRecord) > self.config.minSn
75 negFlux = np.abs(measRecord.get("ip_diffim_PsfDipoleFlux_neg_instFlux"))
76 negFluxFlag = measRecord.get("ip_diffim_PsfDipoleFlux_neg_flag")
77 posFlux = np.abs(measRecord.get("ip_diffim_PsfDipoleFlux_pos_instFlux"))
78 posFluxFlag = measRecord.get("ip_diffim_PsfDipoleFlux_pos_flag")
79
80 if negFluxFlag or posFluxFlag:
81 self.failfail(measRecord)
82 # continue on to classify
83
84 totalFlux = negFlux + posFlux
85
86 # If negFlux or posFlux are NaN, these evaluate to False
87 passesFluxNeg = (negFlux / totalFlux) < self.config.maxFluxRatio
88 passesFluxPos = (posFlux / totalFlux) < self.config.maxFluxRatio
89 if (passesSn and passesFluxPos and passesFluxNeg):
90 val = 1.0
91 else:
92 val = 0.0
93
94 measRecord.set(self.keyProbability, val)
95
96 def fail(self, measRecord, error=None):
97 measRecord.set(self.keyFlag, True)
98
99
101 """Measurement of detected diaSources as dipoles"""
102
103 def setDefaults(self):
104 SingleFrameMeasurementConfig.setDefaults(self)
105 self.pluginsplugins = ["base_CircularApertureFlux",
106 "base_PixelFlags",
107 "base_SkyCoord",
108 "base_PsfFlux",
109 "ip_diffim_NaiveDipoleCentroid",
110 "ip_diffim_NaiveDipoleFlux",
111 "ip_diffim_PsfDipoleFlux",
112 "ip_diffim_ClassificationDipole",
113 ]
114
115 self.slots.calibFlux = None
116 self.slots.modelFlux = None
117 self.slots.gaussianFlux = None
118 self.slots.shape = None
119 self.slots.centroid = "ip_diffim_NaiveDipoleCentroid"
121
122
124 """Measurement of Sources, specifically ones from difference images, for characterization as dipoles
125
126 Parameters
127 ----------
128 sources : 'lsst.afw.table.SourceCatalog'
129 Sources that will be measured
130 badFlags : `list` of `dict`
131 A list of flags that will be used to determine if there was a measurement problem
132
133 """
134 ConfigClass = DipoleMeasurementConfig
135 _DefaultName = "dipoleMeasurement"
136
137
138
141
142class SourceFlagChecker(object):
143 """Functor class to check whether a diaSource has flags set that should cause it to be labeled bad."""
144
145 def __init__(self, sources, badFlags=None):
146 self.badFlags = ['base_PixelFlags_flag_edge', 'base_PixelFlags_flag_interpolatedCenter',
147 'base_PixelFlags_flag_saturatedCenter']
148 if badFlags is not None:
149 for flag in badFlags:
150 self.badFlags.append(flag)
151 self.keys = [sources.getSchema().find(name).key for name in self.badFlags]
152 self.keys.append(sources.table.getCentroidFlagKey())
153
154 def __call__(self, source):
155 """Call the source flag checker on a single Source
156
157 Parameters
158 ----------
159 source :
160 Source that will be examined
161 """
162 for k in self.keys:
163 if source.get(k):
164 return False
165 return True
166
167
168class DipoleAnalysis(object):
169 """Functor class that provides (S/N, position, orientation) of measured dipoles"""
170
171 def __init__(self):
172 pass
173
174 def __call__(self, source):
175 """Parse information returned from dipole measurement
176
177 Parameters
178 ----------
179 source : `lsst.afw.table.SourceRecord`
180 The source that will be examined"""
181 return self.getSn(source), self.getCentroid(source), self.getOrientation(source)
182
183 def getSn(self, source):
184 """Get the total signal-to-noise of the dipole; total S/N is from positive and negative lobe
185
186 Parameters
187 ----------
188 source : `lsst.afw.table.SourceRecord`
189 The source that will be examined"""
190
191 posflux = source.get("ip_diffim_PsfDipoleFlux_pos_instFlux")
192 posfluxErr = source.get("ip_diffim_PsfDipoleFlux_pos_instFluxErr")
193 negflux = source.get("ip_diffim_PsfDipoleFlux_neg_instFlux")
194 negfluxErr = source.get("ip_diffim_PsfDipoleFlux_neg_instFluxErr")
195
196 # Not a dipole!
197 if (posflux < 0) is (negflux < 0):
198 return 0
199
200 return np.sqrt((posflux/posfluxErr)**2 + (negflux/negfluxErr)**2)
201
202 def getCentroid(self, source):
203 """Get the centroid of the dipole; average of positive and negative lobe
204
205 Parameters
206 ----------
207 source : `lsst.afw.table.SourceRecord`
208 The source that will be examined"""
209
210 negCenX = source.get("ip_diffim_PsfDipoleFlux_neg_centroid_x")
211 negCenY = source.get("ip_diffim_PsfDipoleFlux_neg_centroid_y")
212 posCenX = source.get("ip_diffim_PsfDipoleFlux_pos_centroid_x")
213 posCenY = source.get("ip_diffim_PsfDipoleFlux_pos_centroid_y")
214 if (np.isinf(negCenX) or np.isinf(negCenY) or np.isinf(posCenX) or np.isinf(posCenY)):
215 return None
216
217 center = geom.Point2D(0.5*(negCenX+posCenX),
218 0.5*(negCenY+posCenY))
219 return center
220
221 def getOrientation(self, source):
222 """Calculate the orientation of dipole; vector from negative to positive lobe
223
224 Parameters
225 ----------
226 source : `lsst.afw.table.SourceRecord`
227 The source that will be examined"""
228
229 negCenX = source.get("ip_diffim_PsfDipoleFlux_neg_centroid_x")
230 negCenY = source.get("ip_diffim_PsfDipoleFlux_neg_centroid_y")
231 posCenX = source.get("ip_diffim_PsfDipoleFlux_pos_centroid_x")
232 posCenY = source.get("ip_diffim_PsfDipoleFlux_pos_centroid_y")
233 if (np.isinf(negCenX) or np.isinf(negCenY) or np.isinf(posCenX) or np.isinf(posCenY)):
234 return None
235
236 dx, dy = posCenX-negCenX, posCenY-negCenY
237 angle = geom.Angle(np.arctan2(dx, dy), geom.radians)
238 return angle
239
240 def displayDipoles(self, exposure, sources):
241 """Display debugging information on the detected dipoles
242
243 Parameters
244 ----------
245 exposure : `lsst.afw.image.Exposure`
246 Image the dipoles were measured on
247 sources : `lsst.afw.table.SourceCatalog`
248 The set of diaSources that were measured"""
249
250 import lsstDebug
251 display = lsstDebug.Info(__name__).display
252 displayDiaSources = lsstDebug.Info(__name__).displayDiaSources
253 maskTransparency = lsstDebug.Info(__name__).maskTransparency
254 if not maskTransparency:
255 maskTransparency = 90
256 disp = afwDisplay.Display(frame=lsstDebug.frame)
257 disp.setMaskTransparency(maskTransparency)
258 disp.mtv(exposure)
259
260 if display and displayDiaSources:
261 with disp.Buffering():
262 for source in sources:
263 cenX, cenY = source.get("ipdiffim_DipolePsfFlux_centroid")
264 if np.isinf(cenX) or np.isinf(cenY):
265 cenX, cenY = source.getCentroid()
266
267 isdipole = source.get("ip_diffim_ClassificationDipole_value")
268 if isdipole and np.isfinite(isdipole):
269 # Dipole
270 ctype = afwDisplay.GREEN
271 else:
272 # Not dipole
273 ctype = afwDisplay.RED
274
275 disp.dot("o", cenX, cenY, size=2, ctype=ctype)
276
277 negCenX = source.get("ip_diffim_PsfDipoleFlux_neg_centroid_x")
278 negCenY = source.get("ip_diffim_PsfDipoleFlux_neg_centroid_y")
279 posCenX = source.get("ip_diffim_PsfDipoleFlux_pos_centroid_x")
280 posCenY = source.get("ip_diffim_PsfDipoleFlux_pos_centroid_y")
281 if (np.isinf(negCenX) or np.isinf(negCenY) or np.isinf(posCenX) or np.isinf(posCenY)):
282 continue
283
284 disp.line([(negCenX, negCenY), (posCenX, posCenY)], ctype=afwDisplay.YELLOW)
285
286 lsstDebug.frame += 1
287
288
289class DipoleDeblender(object):
290 """Functor to deblend a source as a dipole, and return a new source with deblended footprints.
291
292 This necessarily overrides some of the functionality from
293 meas_algorithms/python/lsst/meas/algorithms/deblend.py since we
294 need a single source that contains the blended peaks, not
295 multiple children sources. This directly calls the core
296 deblending code deblendBaseline.deblend (optionally _fitPsf for
297 debugging).
298
299 Not actively being used, but there is a unit test for it in
300 dipoleAlgorithm.py.
301 """
302
303 def __init__(self):
304 # Set up defaults to send to deblender
305
306 # Always deblend as Psf
307 self.psfChisqCut1 = self.psfChisqCut2 = self.psfChisqCut2b = np.inf
308 self.log = getLogger('lsst.ip.diffim.DipoleDeblender')
309 self.sigma2fwhm = 2. * np.sqrt(2. * np.log(2.))
310
311 def __call__(self, source, exposure):
312 fp = source.getFootprint()
313 peaks = fp.getPeaks()
314 peaksF = [pk.getF() for pk in peaks]
315 fbb = fp.getBBox()
316 fmask = afwImage.Mask(fbb)
317 fmask.setXY0(fbb.getMinX(), fbb.getMinY())
318 fp.spans.setMask(fmask, 1)
319
320 psf = exposure.getPsf()
321 psfSigPix = psf.computeShape(psf.getAveragePosition()).getDeterminantRadius()
322 psfFwhmPix = psfSigPix * self.sigma2fwhm
323 subimage = afwImage.ExposureF(exposure, bbox=fbb, deep=True)
324 cpsf = deblendBaseline.CachingPsf(psf)
325
326 # if fewer than 2 peaks, just return a copy of the source
327 if len(peaks) < 2:
328 return source.getTable().copyRecord(source)
329
330 # make sure you only deblend 2 peaks; take the brighest and faintest
331 speaks = [(p.getPeakValue(), p) for p in peaks]
332 speaks.sort()
333 dpeaks = [speaks[0][1], speaks[-1][1]]
334
335 # and only set these peaks in the footprint (peaks is mutable)
336 peaks.clear()
337 for peak in dpeaks:
338 peaks.append(peak)
339
340 if True:
341 # Call top-level deblend task
342 fpres = deblendBaseline.deblend(fp, exposure.getMaskedImage(), psf, psfFwhmPix,
343 log=self.log,
344 psfChisqCut1=self.psfChisqCut1,
345 psfChisqCut2=self.psfChisqCut2,
346 psfChisqCut2b=self.psfChisqCut2b)
347 else:
348 # Call lower-level _fit_psf task
349
350 # Prepare results structure
351 fpres = deblendBaseline.DeblenderResult(fp, exposure.getMaskedImage(), psf, psfFwhmPix, self.log)
352
353 for pki, (pk, pkres, pkF) in enumerate(zip(dpeaks, fpres.deblendedParents[0].peaks, peaksF)):
354 self.log.debug('Peak %i', pki)
355 deblendBaseline._fitPsf(fp, fmask, pk, pkF, pkres, fbb, dpeaks, peaksF, self.log,
356 cpsf, psfFwhmPix,
357 subimage.image,
358 subimage.variance,
360
361 deblendedSource = source.getTable().copyRecord(source)
362 deblendedSource.setParent(source.getId())
363 peakList = deblendedSource.getFootprint().getPeaks()
364 peakList.clear()
365
366 for i, peak in enumerate(fpres.deblendedParents[0].peaks):
367 if peak.psfFitFlux > 0:
368 suffix = "pos"
369 else:
370 suffix = "neg"
371 c = peak.psfFitCenter
372 self.log.info("deblended.centroid.dipole.psf.%s %f %f",
373 suffix, c[0], c[1])
374 self.log.info("deblended.chi2dof.dipole.%s %f",
375 suffix, peak.psfFitChisq / peak.psfFitDof)
376 self.log.info("deblended.flux.dipole.psf.%s %f",
377 suffix, peak.psfFitFlux * np.sum(peak.templateImage.array))
378 peakList.append(peak.peak)
379 return deblendedSource
Represent a 2-dimensional array of bitmask pixels.
Definition Mask.h:77
A class representing an angle.
Definition Angle.h:128
fail(self, measRecord, error=None)