22 from __future__
import absolute_import, division, print_function
24 from builtins
import zip
25 from builtins
import object
28 import lsst.afw.geom
as afwGeom
29 import lsst.afw.image
as afwImage
30 import lsst.afw.detection
as afwDetect
31 import lsst.pex.config
as pexConfig
32 from lsst.log
import Log
33 import lsst.meas.deblender.baseline
as deblendBaseline
34 from lsst.meas.base.pluginRegistry
import register
35 from lsst.meas.base
import SingleFrameMeasurementTask, SingleFrameMeasurementConfig, \
36 SingleFramePluginConfig, SingleFramePlugin
37 import lsst.afw.display.ds9
as ds9
39 __all__ = (
"DipoleMeasurementConfig",
"DipoleMeasurementTask",
"DipoleAnalysis",
"DipoleDeblender",
40 "SourceFlagChecker",
"ClassificationDipoleConfig",
"ClassificationDipolePlugin")
44 """Configuration for classification of detected diaSources as dipole or not"""
45 minSn = pexConfig.Field(
46 doc=
"Minimum quadrature sum of positive+negative lobe S/N to be considered a dipole",
47 dtype=float, default=np.sqrt(2) * 5.0,
49 maxFluxRatio = pexConfig.Field(
50 doc=
"Maximum flux ratio in either lobe to be considered a dipole",
51 dtype=float, default=0.65
55 @
register(
"ip_diffim_ClassificationDipole")
57 """A plugin to classify whether a diaSource is a dipole.
60 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_flux"))
76 negFluxFlag = measRecord.get(
"ip_diffim_PsfDipoleFlux_neg_flag")
77 posFlux = np.abs(measRecord.get(
"ip_diffim_PsfDipoleFlux_pos_flux"))
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.instFlux =
None
118 self.slots.shape =
None
119 self.slots.centroid =
"ip_diffim_NaiveDipoleCentroid"
132 \anchor DipoleMeasurementTask_
134 \brief Measurement of Sources, specifically ones from difference images, for characterization as dipoles
136 \section ip_diffim_dipolemeas_Contents Contents
138 - \ref ip_diffim_dipolemeas_Purpose
139 - \ref ip_diffim_dipolemeas_Initialize
140 - \ref ip_diffim_dipolemeas_IO
141 - \ref ip_diffim_dipolemeas_Config
142 - \ref ip_diffim_dipolemeas_Metadata
143 - \ref ip_diffim_dipolemeas_Debug
144 - \ref ip_diffim_dipolemeas_Example
146 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
148 \section ip_diffim_dipolemeas_Purpose Description
150 This class provides a default configuration for running Source measurement on image differences.
152 These default plugins include:
153 \dontinclude dipoleMeasurement.py
154 \skip class DipoleMeasurementConfig
155 @until self.doReplaceWithNoise
157 These plugins enabled by default allow the user to test the hypothesis that the Source is a dipole.
158 This includes a set of measurements derived from intermediate base classes
159 DipoleCentroidAlgorithm and DipoleFluxAlgorithm. Their respective algorithm control classes are defined in
160 DipoleCentroidControl and DipoleFluxControl. Each centroid and flux measurement will have _neg (negative)
161 and _pos (positive lobe) fields.
163 The first set of measurements uses a "naive" alrogithm for centroid and flux measurements, implemented in
164 NaiveDipoleCentroidControl and NaiveDipoleFluxControl. The algorithm uses a naive 3x3 weighted moment around
165 the nominal centroids of each peak in the Source Footprint. These algorithms fill the table fields
166 ip_diffim_NaiveDipoleCentroid* and ip_diffim_NaiveDipoleFlux*
168 The second set of measurements undertakes a joint-Psf model on the negative and positive lobe simultaneously.
169 This fit simultaneously solves for the negative and positive lobe centroids and fluxes using non-linear
170 least squares minimization. The fields are stored in table elements ip_diffim_PsfDipoleFlux*.
172 Because this Task is just a config for SourceMeasurementTask, the same result may be acheived by manually
173 editing the config and running SourceMeasurementTask. For example:
176 config = SingleFrameMeasurementConfig()
177 config.plugins.names = ["base_PsfFlux",
178 "ip_diffim_PsfDipoleFlux",
179 "ip_diffim_NaiveDipoleFlux",
180 "ip_diffim_NaiveDipoleCentroid",
181 "ip_diffim_ClassificationDipole",
182 "base_CircularApertureFlux",
185 config.slots.calibFlux = None
186 config.slots.modelFlux = None
187 config.slots.instFlux = None
188 config.slots.shape = None
189 config.slots.centroid = "ip_diffim_NaiveDipoleCentroid"
190 config.doReplaceWithNoise = False
192 schema = afwTable.SourceTable.makeMinimalSchema()
193 task = SingleFrameMeasurementTask(schema, config=config)
195 task.run(sources, exposure)
199 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
201 \section ip_diffim_dipolemeas_Initialize Task initialization
203 \copydoc \_\_init\_\_
205 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
207 \section ip_diffim_dipolemeas_IO Invoking the Task
211 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
213 \section ip_diffim_dipolemeas_Config Configuration parameters
215 See \ref DipoleMeasurementConfig
217 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
219 \section ip_diffim_dipolemeas_Metadata Quantities set in Metadata
221 No specific values are set in the Task metadata. However, the Source schema are modified to store the
222 results of the dipole-specific measurements.
225 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
227 \section ip_diffim_dipolemeas_Debug Debug variables
229 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
230 flag \c -d/--debug to import \b debug.py from your \c PYTHONPATH. The relevant contents of debug.py
231 for this Task include:
237 di = lsstDebug.getInfo(name)
238 if name == "lsst.ip.diffim.dipoleMeasurement":
239 di.display = True # enable debug output
240 di.maskTransparency = 90 # ds9 mask transparency
241 di.displayDiaSources = True # show exposure with dipole results
243 lsstDebug.Info = DebugInfo
247 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
249 \section ip_diffim_dipolemeas_Example A complete example of using DipoleMeasurementTask
251 This code is dipoleMeasTask.py in the examples directory, and can be run as \em e.g.
253 examples/dipoleMeasTask.py
254 examples/dipoleMeasTask.py --debug
255 examples/dipoleMeasTask.py --debug --image /path/to/image.fits
258 \dontinclude dipoleMeasTask.py
259 Start the processing by parsing the command line, where the user has the option of enabling debugging output
260 and/or sending their own image for demonstration (in case they have not downloaded the afwdata package).
264 \dontinclude dipoleMeasTask.py
265 The processing occurs in the run function. We first extract an exposure from disk or afwdata, displaying
270 Create a default source schema that we will append fields to as we add more algorithms:
271 \skip makeMinimalSchema
272 @until makeMinimalSchema
274 Create the detection and measurement Tasks, with some minor tweaking of their configs:
278 Having fully initialied the schema, we create a Source table from it:
286 Because we are looking for dipoles, we need to merge the positive and negative detections:
290 Finally, perform measurement (both standard and dipole-specialized) on the merged sources:
294 Optionally display debugging information:
296 @until displayDipoles
297 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
300 ConfigClass = DipoleMeasurementConfig
301 _DefaultName =
"dipoleMeasurement"
309 """!Functor class to check whether a diaSource has flags set that should cause it to be labeled bad."""
314 @param sources Sources that will be measured
315 @param badFlags A list of flags that will be used to determine if there was a measurement problem
317 The list of badFlags will be used to make a list of keys to check for measurement flags on. By
318 default the centroid keys are added to this list"""
320 self.
badFlags = [
'base_PixelFlags_flag_edge',
'base_PixelFlags_flag_interpolatedCenter',
321 'base_PixelFlags_flag_saturatedCenter']
322 if badFlags
is not None:
323 for flag
in badFlags:
324 self.badFlags.append(flag)
325 self.
keys = [sources.getSchema().find(name).key
for name
in self.
badFlags]
326 self.keys.append(sources.table.getCentroidFlagKey())
329 """!Call the source flag checker on a single Source
331 @param source Source that will be examined"""
339 """!Functor class that provides (S/N, position, orientation) of measured dipoles"""
346 """!Parse information returned from dipole measurement
348 @param source The source that will be examined"""
349 return self.getSn(source), self.getCentroid(source), self.getOrientation(source)
352 """!Get the total signal-to-noise of the dipole; total S/N is from positive and negative lobe
354 @param source The source that will be examined"""
356 posflux = source.get(
"ip_diffim_PsfDipoleFlux_pos_flux")
357 posfluxErr = source.get(
"ip_diffim_PsfDipoleFlux_pos_fluxSigma")
358 negflux = source.get(
"ip_diffim_PsfDipoleFlux_neg_flux")
359 negfluxErr = source.get(
"ip_diffim_PsfDipoleFlux_neg_fluxSigma")
362 if (posflux < 0)
is (negflux < 0):
365 return np.sqrt((posflux/posfluxErr)**2 + (negflux/negfluxErr)**2)
368 """!Get the centroid of the dipole; average of positive and negative lobe
370 @param source The source that will be examined"""
372 negCenX = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_x")
373 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
374 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
375 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
376 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
379 center = afwGeom.Point2D(0.5*(negCenX+posCenX),
380 0.5*(negCenY+posCenY))
384 """!Calculate the orientation of dipole; vector from negative to positive lobe
386 @param source The source that will be examined"""
388 negCenX = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_x")
389 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
390 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
391 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
392 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
395 dx, dy = posCenX-negCenX, posCenY-negCenY
396 angle = afwGeom.Angle(np.arctan2(dx, dy), afwGeom.radians)
400 """!Display debugging information on the detected dipoles
402 @param exposure Image the dipoles were measured on
403 @param sources The set of diaSources that were measured"""
406 display = lsstDebug.Info(__name__).display
407 displayDiaSources = lsstDebug.Info(__name__).displayDiaSources
408 maskTransparency = lsstDebug.Info(__name__).maskTransparency
409 if not maskTransparency:
410 maskTransparency = 90
411 ds9.setMaskTransparency(maskTransparency)
412 ds9.mtv(exposure, frame=lsstDebug.frame)
414 if display
and displayDiaSources:
415 with ds9.Buffering():
416 for source
in sources:
417 cenX, cenY = source.get(
"ipdiffim_DipolePsfFlux_centroid")
418 if np.isinf(cenX)
or np.isinf(cenY):
419 cenX, cenY = source.getCentroid()
421 isdipole = source.get(
"classification.dipole")
422 if isdipole
and np.isfinite(isdipole):
429 ds9.dot(
"o", cenX, cenY, size=2, ctype=ctype, frame=lsstDebug.frame)
431 negCenX = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_x")
432 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
433 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
434 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
435 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
438 ds9.line([(negCenX, negCenY), (posCenX, posCenY)], ctype=
"yellow", frame=lsstDebug.frame)
444 """!Functor to deblend a source as a dipole, and return a new source with deblended footprints.
446 This necessarily overrides some of the functionality from
447 meas_algorithms/python/lsst/meas/algorithms/deblend.py since we
448 need a single source that contains the blended peaks, not
449 multiple children sources. This directly calls the core
450 deblending code deblendBaseline.deblend (optionally _fitPsf for
453 Not actively being used, but there is a unit test for it in
462 self.
log = Log.getLogger(
'ip.diffim.DipoleDeblender')
466 fp = source.getFootprint()
467 peaks = fp.getPeaks()
468 peaksF = [pk.getF()
for pk
in peaks]
470 fmask = afwImage.MaskU(fbb)
471 fmask.setXY0(fbb.getMinX(), fbb.getMinY())
472 fp.spans.setMask(fmask, 1)
474 psf = exposure.getPsf()
475 psfSigPix = psf.computeShape().getDeterminantRadius()
477 subimage = afwImage.ExposureF(exposure, bbox=fbb, deep=
True)
478 cpsf = deblendBaseline.CachingPsf(psf)
482 return source.getTable().copyRecord(source)
485 speaks = [(p.getPeakValue(), p)
for p
in peaks]
487 dpeaks = [speaks[0][1], speaks[-1][1]]
496 fpres = deblendBaseline.deblend(fp, exposure.getMaskedImage(), psf, psfFwhmPix,
505 fpres = deblendBaseline.DeblenderResult(fp, exposure.getMaskedImage(), psf, psfFwhmPix, self.
log)
507 for pki, (pk, pkres, pkF)
in enumerate(zip(dpeaks, fpres.deblendedParents[0].peaks, peaksF)):
508 self.log.debug(
'Peak %i', pki)
509 deblendBaseline._fitPsf(fp, fmask, pk, pkF, pkres, fbb, dpeaks, peaksF, self.
log,
511 subimage.getMaskedImage().getImage(),
512 subimage.getMaskedImage().getVariance(),
515 deblendedSource = source.getTable().copyRecord(source)
516 deblendedSource.setParent(source.getId())
517 peakList = deblendedSource.getFootprint().getPeaks()
520 for i, peak
in enumerate(fpres.deblendedParents[0].peaks):
521 if peak.psfFitFlux > 0:
525 c = peak.psfFitCenter
526 self.log.info(
"deblended.centroid.dipole.psf.%s %f %f",
528 self.log.info(
"deblended.chi2dof.dipole.%s %f",
529 suffix, peak.psfFitChisq / peak.psfFitDof)
530 self.log.info(
"deblended.flux.dipole.psf.%s %f",
531 suffix, peak.psfFitFlux * np.sum(peak.templateImage.getArray()))
532 peakList.append(peak.peak)
533 return deblendedSource
def getCentroid
Get the centroid of the dipole; average of positive and negative lobe.
def displayDipoles
Display debugging information on the detected dipoles.
Measurement of Sources, specifically ones from difference images, for characterization as dipoles...
Functor class that provides (S/N, position, orientation) of measured dipoles.
Functor to deblend a source as a dipole, and return a new source with deblended footprints.
def __call__
Parse information returned from dipole measurement.
Functor class to check whether a diaSource has flags set that should cause it to be labeled bad...
Measurement of detected diaSources as dipoles.
def getOrientation
Calculate the orientation of dipole; vector from negative to positive lobe.
def getSn
Get the total signal-to-noise of the dipole; total S/N is from positive and negative lobe...
def __call__
Call the source flag checker on a single Source.