30 from lsst.meas.base import SingleFrameMeasurementTask, SingleFrameMeasurementConfig, \
31 SingleFramePluginConfig, SingleFramePlugin
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
59 return cls.APCORR_ORDER
61 def __init__(self, config, name, schema, metadata):
62 SingleFramePlugin.__init__(self, config, name, schema, metadata)
65 doc=
"Set to 1 for dipoles, else 0.")
66 self.
keyFlag = schema.addField(name +
"_flag", type=
"Flag", doc=
"Set to 1 for any fatal failure.")
69 passesSn = self.dipoleAnalysis.getSn(measRecord) > self.config.minSn
70 negFlux = np.abs(measRecord.get(
"ip_diffim_PsfDipoleFlux_neg_flux"))
71 negFluxFlag = measRecord.get(
"ip_diffim_PsfDipoleFlux_neg_flag")
72 posFlux = np.abs(measRecord.get(
"ip_diffim_PsfDipoleFlux_pos_flux"))
73 posFluxFlag = measRecord.get(
"ip_diffim_PsfDipoleFlux_pos_flag")
75 if negFluxFlag
or posFluxFlag:
79 totalFlux = negFlux + posFlux
82 passesFluxNeg = (negFlux / totalFlux) < self.config.maxFluxRatio
83 passesFluxPos = (posFlux / totalFlux) < self.config.maxFluxRatio
84 if (passesSn
and passesFluxPos
and passesFluxNeg):
91 def fail(self, measRecord, error=None):
92 measRecord.set(self.
keyFlag,
True)
96 """!Measurement of detected diaSources as dipoles"""
99 SingleFrameMeasurementConfig.setDefaults(self)
104 "ip_diffim_NaiveDipoleCentroid",
105 "ip_diffim_NaiveDipoleFlux",
106 "ip_diffim_PsfDipoleFlux",
107 "ip_diffim_ClassificationDipole",
110 self.slots.calibFlux =
None
111 self.slots.modelFlux =
None
112 self.slots.instFlux =
None
113 self.slots.shape =
None
114 self.slots.centroid =
"ip_diffim_NaiveDipoleCentroid"
125 \anchor DipoleMeasurementTask_
127 \brief Measurement of Sources, specifically ones from difference images, for characterization as dipoles
129 \section ip_diffim_dipolemeas_Contents Contents
131 - \ref ip_diffim_dipolemeas_Purpose
132 - \ref ip_diffim_dipolemeas_Initialize
133 - \ref ip_diffim_dipolemeas_IO
134 - \ref ip_diffim_dipolemeas_Config
135 - \ref ip_diffim_dipolemeas_Metadata
136 - \ref ip_diffim_dipolemeas_Debug
137 - \ref ip_diffim_dipolemeas_Example
139 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
141 \section ip_diffim_dipolemeas_Purpose Description
143 This class provides a default configuration for running Source measurement on image differences.
145 These default plugins include:
146 \dontinclude dipoleMeasurement.py
147 \skip class DipoleMeasurementConfig
148 \until self.doReplaceWithNoise
150 These plugins enabled by default allow the user to test the hypothesis that the Source is a dipole.
151 This includes a set of measurements derived from intermediate base classes
152 DipoleCentroidAlgorithm and DipoleFluxAlgorithm. Their respective algorithm control classes are defined in
153 DipoleCentroidControl and DipoleFluxControl. Each centroid and flux measurement will have _neg (negative)
154 and _pos (positive lobe) fields.
156 The first set of measurements uses a "naive" alrogithm for centroid and flux measurements, implemented in
157 NaiveDipoleCentroidControl and NaiveDipoleFluxControl. The algorithm uses a naive 3x3 weighted moment around
158 the nominal centroids of each peak in the Source Footprint. These algorithms fill the table fields
159 ip_diffim_NaiveDipoleCentroid* and ip_diffim_NaiveDipoleFlux*
161 The second set of measurements undertakes a joint-Psf model on the negative and positive lobe simultaneously.
162 This fit simultaneously solves for the negative and positive lobe centroids and fluxes using non-linear
163 least squares minimization. The fields are stored in table elements ip_diffim_PsfDipoleFlux*.
165 Because this Task is just a config for SourceMeasurementTask, the same result may be acheived by manually
166 editing the config and running SourceMeasurementTask. For example:
169 config = SingleFrameMeasurementConfig()
170 config.plugins.names = ["base_PsfFlux",
171 "ip_diffim_PsfDipoleFlux",
172 "ip_diffim_NaiveDipoleFlux",
173 "ip_diffim_NaiveDipoleCentroid",
174 "ip_diffim_ClassificationDipole",
175 "base_CircularApertureFlux",
178 config.slots.calibFlux = None
179 config.slots.modelFlux = None
180 config.slots.instFlux = None
181 config.slots.shape = None
182 config.slots.centroid = "ip_diffim_NaiveDipoleCentroid"
183 config.doReplaceWithNoise = False
185 schema = afwTable.SourceTable.makeMinimalSchema()
186 task = SingleFrameMeasurementTask(schema, config=config)
188 task.run(sources, exposure)
192 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
194 \section ip_diffim_dipolemeas_Initialize Task initialization
196 \copydoc \_\_init\_\_
198 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
200 \section ip_diffim_dipolemeas_IO Invoking the Task
204 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
206 \section ip_diffim_dipolemeas_Config Configuration parameters
208 See \ref DipoleMeasurementConfig
210 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
212 \section ip_diffim_dipolemeas_Metadata Quantities set in Metadata
214 No specific values are set in the Task metadata. However, the Source schema are modified to store the
215 results of the dipole-specific measurements.
218 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
220 \section ip_diffim_dipolemeas_Debug Debug variables
222 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
223 flag \c -d/--debug to import \b debug.py from your \c PYTHONPATH. The relevant contents of debug.py
224 for this Task include:
230 di = lsstDebug.getInfo(name)
231 if name == "lsst.ip.diffim.dipoleMeasurement":
232 di.display = True # enable debug output
233 di.maskTransparency = 90 # ds9 mask transparency
234 di.displayDiaSources = True # show exposure with dipole results
236 lsstDebug.Info = DebugInfo
240 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
242 \section ip_diffim_dipolemeas_Example A complete example of using DipoleMeasurementTask
244 This code is dipoleMeasTask.py in the examples directory, and can be run as \em e.g.
246 examples/dipoleMeasTask.py
247 examples/dipoleMeasTask.py --debug
248 examples/dipoleMeasTask.py --debug --image /path/to/image.fits
251 \dontinclude dipoleMeasTask.py
252 Start the processing by parsing the command line, where the user has the option of enabling debugging output
253 and/or sending their own image for demonstration (in case they have not downloaded the afwdata package).
257 \dontinclude dipoleMeasTask.py
258 The processing occurs in the run function. We first extract an exposure from disk or afwdata, displaying
263 Create a default source schema that we will append fields to as we add more algorithms:
264 \skip makeMinimalSchema
265 \until makeMinimalSchema
267 Create the detection and measurement Tasks, with some minor tweaking of their configs:
271 Having fully initialied the schema, we create a Source table from it:
279 Because we are looking for dipoles, we need to merge the positive and negative detections:
283 Finally, perform measurement (both standard and dipole-specialized) on the merged sources:
287 Optionally display debugging information:
289 \until displayDipoles
290 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
293 ConfigClass = DipoleMeasurementConfig
294 _DefaultName =
"dipoleMeasurement"
302 """!Functor class to check whether a diaSource has flags set that should cause it to be labeled bad."""
306 @param sources Sources that will be measured
307 @param badFlags A list of flags that will be used to determine if there was a measurement problem
309 The list of badFlags will be used to make a list of keys to check for measurement flags on. By
310 default the centroid keys are added to this list"""
312 self.
badFlags = [
'base_PixelFlags_flag_edge',
'base_PixelFlags_flag_interpolatedCenter',
313 'base_PixelFlags_flag_saturatedCenter']
314 if badFlags
is not None:
315 for flag
in badFlags:
316 self.badFlags.append(flag)
317 self.
keys = [sources.getSchema().find(name).key
for name
in self.
badFlags]
318 self.keys.append(sources.table.getCentroidFlagKey())
321 """!Call the source flag checker on a single Source
323 @param source Source that will be examined"""
330 """!Functor class that provides (S/N, position, orientation) of measured dipoles"""
336 """!Parse information returned from dipole measurement
338 @param source The source that will be examined"""
339 return self.getSn(source), self.getCentroid(source), self.getOrientation(source)
342 """!Get the total signal-to-noise of the dipole; total S/N is from positive and negative lobe
344 @param source The source that will be examined"""
346 posflux = source.get(
"ip_diffim_PsfDipoleFlux_pos_flux")
347 posfluxErr = source.get(
"ip_diffim_PsfDipoleFlux_pos_fluxSigma")
348 negflux = source.get(
"ip_diffim_PsfDipoleFlux_neg_flux")
349 negfluxErr = source.get(
"ip_diffim_PsfDipoleFlux_neg_fluxSigma")
352 if (posflux < 0)
is (negflux < 0):
355 return np.sqrt((posflux/posfluxErr)**2 + (negflux/negfluxErr)**2)
358 """!Get the centroid of the dipole; average of positive and negative lobe
360 @param source The source that will be examined"""
362 negCenX = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_x")
363 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
364 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
365 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
366 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
370 0.5*(negCenY+posCenY))
374 """!Calculate the orientation of dipole; vector from negative to positive lobe
376 @param source The source that will be examined"""
378 negCenX = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_x")
379 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
380 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
381 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
382 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
385 dx, dy = posCenX-negCenX, posCenY-negCenY
390 """!Display debugging information on the detected dipoles
392 @param exposure Image the dipoles were measured on
393 @param sources The set of diaSources that were measured"""
399 if not maskTransparency:
400 maskTransparency = 90
401 ds9.setMaskTransparency(maskTransparency)
402 ds9.mtv(exposure, frame=lsstDebug.frame)
404 if display
and displayDiaSources:
405 with ds9.Buffering():
406 for source
in sources:
407 cenX, cenY = source.get(
"ipdiffim_DipolePsfFlux_centroid")
408 if np.isinf(cenX)
or np.isinf(cenY):
409 cenX, cenY = source.getCentroid()
411 isdipole = source.get(
"classification.dipole")
412 if isdipole
and np.isfinite(isdipole):
419 ds9.dot(
"o", cenX, cenY, size=2, ctype=ctype, frame=lsstDebug.frame)
421 negCenX = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_x")
422 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
423 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
424 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
425 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
428 ds9.line([(negCenX, negCenY), (posCenX, posCenY)], ctype=
"yellow", frame=lsstDebug.frame)
435 """!Functor to deblend a source as a dipole, and return a new source with deblended footprints.
437 This necessarily overrides some of the functionality from
438 meas_algorithms/python/lsst/meas/algorithms/deblend.py since we
439 need a single source that contains the blended peaks, not
440 multiple children sources. This directly calls the core
441 deblending code deblendBaseline.deblend (optionally _fitPsf for
444 Not actively being used, but there is a unit test for it in
452 self.
log = Log.getLogger(
'lsst.ip.diffim.DipoleDeblender')
453 self.log.setLevel(Log.INFO)
457 fp = source.getFootprint()
458 peaks = fp.getPeaks()
459 peaksF = [pk.getF()
for pk
in peaks]
461 fmask = afwImage.MaskU(fbb)
462 fmask.setXY0(fbb.getMinX(), fbb.getMinY())
465 psf = exposure.getPsf()
466 psfSigPix = psf.computeShape().getDeterminantRadius()
468 subimage = afwImage.ExposureF(exposure, fbb,
True)
469 cpsf = deblendBaseline.CachingPsf(psf)
473 return source.getTable().copyRecord(source)
476 speaks = [(p.getPeakValue(), p)
for p
in peaks]
478 dpeaks = [speaks[0][1], speaks[-1][1]]
487 fpres = deblendBaseline.deblend(fp, exposure.getMaskedImage(), psf, psfFwhmPix,
496 fpres = deblendBaseline.PerFootprint()
498 for pki,pk
in enumerate(dpeaks):
499 pkres = deblendBaseline.PerPeak()
502 fpres.peaks.append(pkres)
504 for pki,(pk,pkres,pkF)
in enumerate(zip(dpeaks, fpres.peaks, peaksF)):
505 self.log.debug(
'Peak %i', pki)
506 deblendBaseline._fitPsf(fp, fmask, pk, pkF, pkres, fbb, dpeaks, peaksF, self.
log,
508 subimage.getMaskedImage().getImage(),
509 subimage.getMaskedImage().getVariance(),
513 deblendedSource = source.getTable().copyRecord(source)
514 deblendedSource.setParent(source.getId())
515 peakList = deblendedSource.getFootprint().getPeaks()
518 for i, peak
in enumerate(fpres.peaks):
519 if peak.psfFitFlux > 0:
523 c = peak.psfFitCenter
524 self.log.info(
"deblended.centroid.dipole.psf.%s %f %f",
526 self.log.info(
"deblended.chi2dof.dipole.%s %f",
527 suffix, peak.psfFitChisq / peak.psfFitDof)
528 self.log.info(
"deblended.flux.dipole.psf.%s %f",
529 suffix, peak.psfFitFlux * np.sum(peak.templateImage.getArray()))
530 peakList.append(peak.peak)
531 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.
MaskT setMaskFromFootprint(lsst::afw::image::Mask< MaskT > *mask, Footprint const &footprint, MaskT const bitmask)
OR bitmask into all the Mask's pixels that are in the Footprint.
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.