27import lsst.meas.deblender.baseline
as deblendBaseline
29from lsst.meas.base import SingleFrameMeasurementTask, SingleFrameMeasurementConfig, \
30 SingleFramePluginConfig, SingleFramePlugin
32from lsst.utils.logging
import getLogger
34__all__ = (
"DipoleMeasurementConfig",
"DipoleMeasurementTask",
"DipoleAnalysis",
"DipoleDeblender",
35 "SourceFlagChecker",
"ClassificationDipoleConfig",
"ClassificationDipolePlugin")
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,
44 maxFluxRatio = pexConfig.Field(
45 doc=
"Maximum flux ratio in either lobe to be considered a dipole",
46 dtype=float, default=0.65
50@register("ip_diffim_ClassificationDipole")
52 """A plugin to classify whether a diaSource is a dipole.
55 ConfigClass = ClassificationDipoleConfig
64 return cls.APCORR_ORDER
66 def __init__(self, config, name, schema, metadata):
67 SingleFramePlugin.__init__(self, config, name, schema, metadata)
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.")
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")
80 if negFluxFlag
or posFluxFlag:
84 totalFlux = negFlux + posFlux
87 passesFluxNeg = (negFlux / totalFlux) < self.config.maxFluxRatio
88 passesFluxPos = (posFlux / totalFlux) < self.config.maxFluxRatio
89 if (passesSn
and passesFluxPos
and passesFluxNeg):
96 def fail(self, measRecord, error=None):
97 measRecord.set(self.
keyFlag,
True)
101 """Measurement of detected diaSources as dipoles"""
104 SingleFrameMeasurementConfig.setDefaults(self)
109 "ip_diffim_NaiveDipoleCentroid",
110 "ip_diffim_NaiveDipoleFlux",
111 "ip_diffim_PsfDipoleFlux",
112 "ip_diffim_ClassificationDipole",
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"
124 """Measurement of Sources, specifically ones from difference images, for characterization as dipoles
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
135 The list of badFlags will be used to make a list of keys to check
for measurement flags on. By
136 default the centroid keys are added to this list
140 This
class provides a default configuration for running Source measurement on image differences.
145 "Measurement of detected diaSources as dipoles"
146 def setDefaults(self):
147 SingleFrameMeasurementConfig.setDefaults(self)
148 self.plugins = [
"base_CircularApertureFlux",
152 "ip_diffim_NaiveDipoleCentroid",
153 "ip_diffim_NaiveDipoleFlux",
154 "ip_diffim_PsfDipoleFlux",
155 "ip_diffim_ClassificationDipole",
157 self.slots.calibFlux =
None
158 self.slots.modelFlux =
None
159 self.slots.instFlux =
None
160 self.slots.shape =
None
161 self.slots.centroid =
"ip_diffim_NaiveDipoleCentroid"
162 self.doReplaceWithNoise =
False
164 These plugins enabled by default allow the user to test the hypothesis that the Source
is a dipole.
165 This includes a set of measurements derived
from intermediate base classes
166 DipoleCentroidAlgorithm
and DipoleFluxAlgorithm.
167 Their respective algorithm control classes are defined
in
168 DipoleCentroidControl
and DipoleFluxControl.
169 Each centroid
and flux measurement will have _neg (negative)
170 and _pos (positive lobe) fields.
172 The first set of measurements uses a
"naive" alrogithm
173 for centroid
and flux measurements, implemented
in
174 NaiveDipoleCentroidControl
and NaiveDipoleFluxControl.
175 The algorithm uses a naive 3x3 weighted moment around
176 the nominal centroids of each peak
in the Source Footprint. These algorithms fill the table fields
177 ip_diffim_NaiveDipoleCentroid*
and ip_diffim_NaiveDipoleFlux*
179 The second set of measurements undertakes a joint-Psf model on the negative
180 and positive lobe simultaneously. This fit simultaneously solves
for the negative
and positive
181 lobe centroids
and fluxes using non-linear least squares minimization.
182 The fields are stored
in table elements ip_diffim_PsfDipoleFlux*.
184 Because this Task
is just a config
for SingleFrameMeasurementTask, the same result may be acheived by
185 manually editing the config
and running SingleFrameMeasurementTask. For example:
189 config = SingleFrameMeasurementConfig()
190 config.plugins.names = [
"base_PsfFlux",
191 "ip_diffim_PsfDipoleFlux",
192 "ip_diffim_NaiveDipoleFlux",
193 "ip_diffim_NaiveDipoleCentroid",
194 "ip_diffim_ClassificationDipole",
195 "base_CircularApertureFlux",
198 config.slots.calibFlux =
None
199 config.slots.modelFlux =
None
200 config.slots.instFlux =
None
201 config.slots.shape =
None
202 config.slots.centroid =
"ip_diffim_NaiveDipoleCentroid"
203 config.doReplaceWithNoise =
False
205 schema = afwTable.SourceTable.makeMinimalSchema()
206 task = SingleFrameMeasurementTask(schema, config=config)-
210 The ``pipetask`` command line interface supports a
211 flag --debug to
import @b debug.py
from your PYTHONPATH. The relevant contents of debug.py
212 for this Task include:
220 if name ==
"lsst.ip.diffim.dipoleMeasurement":
222 di.maskTransparency = 90
223 di.displayDiaSources =
True
228 config.slots.calibFlux =
None
229 config.slots.modelFlux =
None
230 config.slots.gaussianFlux =
None
231 config.slots.shape =
None
232 config.slots.centroid =
"ip_diffim_NaiveDipoleCentroid"
233 config.doReplaceWithNoise =
False
235 Start the processing by parsing the command line, where the user has the option of
236 enabling debugging output
and/
or sending their own image
for demonstration
237 (
in case they have
not downloaded the afwdata package).
241 if __name__ ==
"__main__":
243 parser = argparse.ArgumentParser(
244 description=
"Demonstrate the use of SourceDetectionTask and DipoleMeasurementTask")
245 parser.add_argument(
'--debug',
'-d', action=
"store_true", help=
"Load debug.py?", default=
False)
246 parser.add_argument(
"--image",
"-i", help=
"User defined image", default=
None)
247 args = parser.parse_args()
251 debug.lsstDebug.frame = 2
252 except ImportError
as e:
253 print(e, file=sys.stderr)
256 The processing occurs
in the run function. We first extract an exposure
from disk
or afwdata, displaying
262 exposure = loadData(args.image)
264 afwDisplay.Display(frame=1).mtv(exposure)
266 Create a default source schema that we will append fields to
as we add more algorithms:
270 schema = afwTable.SourceTable.makeMinimalSchema()
272 Create the detection
and measurement Tasks,
with some minor tweaking of their configs:
277 config = SourceDetectionTask.ConfigClass()
278 config.thresholdPolarity =
"both"
279 config.background.isNanSafe =
True
280 config.thresholdValue = 3
281 detectionTask = SourceDetectionTask(config=config, schema=schema)
283 config = DipoleMeasurementTask.ConfigClass()
284 config.plugins.names.remove(
'base_SkyCoord')
288 Having fully initialied the schema, we create a Source table
from it:
293 tab = afwTable.SourceTable.make(schema)
300 results = detectionTask.run(tab, exposure)
302 Because we are looking
for dipoles, we need to merge the positive
and negative detections:
307 fpSet = results.positive
309 fpSet.merge(results.negative, growFootprint, growFootprint,
False)
310 diaSources = afwTable.SourceCatalog(tab)
311 fpSet.makeSources(diaSources)
312 print(
"Merged %s Sources into %d diaSources (from %d +ve, %d -ve)" % (len(results.sources),
317 Finally, perform measurement (both standard
and dipole-specialized) on the merged sources:
321 measureTask.run(diaSources, exposure)
323 Optionally display debugging information:
330 dpa.displayDipoles(exposure, diaSources)
333 ConfigClass = DipoleMeasurementConfig
334 _DefaultName = "dipoleMeasurement"
342 """Functor class to check whether a diaSource has flags set that should cause it to be labeled bad."""
345 self.
badFlags = [
'base_PixelFlags_flag_edge',
'base_PixelFlags_flag_interpolatedCenter',
346 'base_PixelFlags_flag_saturatedCenter']
347 if badFlags
is not None:
348 for flag
in badFlags:
350 self.
keys = [sources.getSchema().find(name).key
for name
in self.
badFlags]
351 self.
keys.append(sources.table.getCentroidFlagKey())
354 """Call the source flag checker on a single Source
359 Source that will be examined
368 """Functor class that provides (S/N, position, orientation) of measured dipoles"""
374 """Parse information returned from dipole measurement
379 The source that will be examined"""
380 return self.getSn(source), self.getCentroid(source), self.getOrientation(source)
383 """Get the total signal-to-noise of the dipole; total S/N is from positive and negative lobe
388 The source that will be examined"""
390 posflux = source.get("ip_diffim_PsfDipoleFlux_pos_instFlux")
391 posfluxErr = source.get(
"ip_diffim_PsfDipoleFlux_pos_instFluxErr")
392 negflux = source.get(
"ip_diffim_PsfDipoleFlux_neg_instFlux")
393 negfluxErr = source.get(
"ip_diffim_PsfDipoleFlux_neg_instFluxErr")
396 if (posflux < 0)
is (negflux < 0):
399 return np.sqrt((posflux/posfluxErr)**2 + (negflux/negfluxErr)**2)
402 """Get the centroid of the dipole; average of positive and negative lobe
407 The source that will be examined"""
409 negCenX = source.get("ip_diffim_PsfDipoleFlux_neg_centroid_x")
410 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
411 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
412 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
413 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
417 0.5*(negCenY+posCenY))
421 """Calculate the orientation of dipole; vector from negative to positive lobe
426 The source that will be examined"""
428 negCenX = source.get("ip_diffim_PsfDipoleFlux_neg_centroid_x")
429 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
430 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
431 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
432 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
435 dx, dy = posCenX-negCenX, posCenY-negCenY
436 angle =
geom.Angle(np.arctan2(dx, dy), geom.radians)
440 """Display debugging information on the detected dipoles
445 Image the dipoles were measured on
447 The set of diaSources that were measured"""
453 if not maskTransparency:
454 maskTransparency = 90
455 disp = afwDisplay.Display(frame=lsstDebug.frame)
456 disp.setMaskTransparency(maskTransparency)
459 if display
and displayDiaSources:
460 with disp.Buffering():
461 for source
in sources:
462 cenX, cenY = source.get(
"ipdiffim_DipolePsfFlux_centroid")
463 if np.isinf(cenX)
or np.isinf(cenY):
464 cenX, cenY = source.getCentroid()
466 isdipole = source.get(
"ip_diffim_ClassificationDipole_value")
467 if isdipole
and np.isfinite(isdipole):
469 ctype = afwDisplay.GREEN
472 ctype = afwDisplay.RED
474 disp.dot(
"o", cenX, cenY, size=2, ctype=ctype)
476 negCenX = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_x")
477 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
478 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
479 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
480 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
483 disp.line([(negCenX, negCenY), (posCenX, posCenY)], ctype=afwDisplay.YELLOW)
489 """Functor to deblend a source as a dipole, and return a new source with deblended footprints.
491 This necessarily overrides some of the functionality from
492 meas_algorithms/python/lsst/meas/algorithms/deblend.py since we
493 need a single source that contains the blended peaks,
not
494 multiple children sources. This directly calls the core
495 deblending code deblendBaseline.deblend (optionally _fitPsf
for
498 Not actively being used, but there
is a unit test
for it
in
507 self.
log = getLogger(
'lsst.ip.diffim.DipoleDeblender')
511 fp = source.getFootprint()
512 peaks = fp.getPeaks()
513 peaksF = [pk.getF()
for pk
in peaks]
516 fmask.setXY0(fbb.getMinX(), fbb.getMinY())
517 fp.spans.setMask(fmask, 1)
519 psf = exposure.getPsf()
520 psfSigPix = psf.computeShape(psf.getAveragePosition()).getDeterminantRadius()
522 subimage = afwImage.ExposureF(exposure, bbox=fbb, deep=
True)
523 cpsf = deblendBaseline.CachingPsf(psf)
527 return source.getTable().copyRecord(source)
530 speaks = [(p.getPeakValue(), p)
for p
in peaks]
532 dpeaks = [speaks[0][1], speaks[-1][1]]
541 fpres = deblendBaseline.deblend(fp, exposure.getMaskedImage(), psf, psfFwhmPix,
550 fpres = deblendBaseline.DeblenderResult(fp, exposure.getMaskedImage(), psf, psfFwhmPix, self.
log)
552 for pki, (pk, pkres, pkF)
in enumerate(zip(dpeaks, fpres.deblendedParents[0].peaks, peaksF)):
553 self.
log.debug(
'Peak %i', pki)
554 deblendBaseline._fitPsf(fp, fmask, pk, pkF, pkres, fbb, dpeaks, peaksF, self.
log,
556 subimage.getMaskedImage().getImage(),
557 subimage.getMaskedImage().getVariance(),
560 deblendedSource = source.getTable().copyRecord(source)
561 deblendedSource.setParent(source.getId())
562 peakList = deblendedSource.getFootprint().getPeaks()
565 for i, peak
in enumerate(fpres.deblendedParents[0].peaks):
566 if peak.psfFitFlux > 0:
570 c = peak.psfFitCenter
571 self.
log.info(
"deblended.centroid.dipole.psf.%s %f %f",
573 self.
log.info(
"deblended.chi2dof.dipole.%s %f",
574 suffix, peak.psfFitChisq / peak.psfFitDof)
575 self.
log.info(
"deblended.flux.dipole.psf.%s %f",
576 suffix, peak.psfFitFlux * np.sum(peak.templateImage.getArray()))
577 peakList.append(peak.peak)
578 return deblendedSource
def fail(self, measRecord, error=None)
def __init__(self, config, name, schema, metadata)
def getExecutionOrder(cls)
def measure(self, measRecord, exposure)
def displayDipoles(self, exposure, sources)
def __call__(self, source)
def getOrientation(self, source)
def getCentroid(self, source)
def __call__(self, source, exposure)
def __init__(self, sources, badFlags=None)
def __call__(self, source)
def run(self, coaddExposures, bbox, wcs, dataIds, **kwargs)