1 from builtins
import zip
2 from builtins
import object
32 from lsst.meas.base import SingleFrameMeasurementTask, SingleFrameMeasurementConfig, \
33 SingleFramePluginConfig, SingleFramePlugin
36 __all__ = (
"DipoleMeasurementConfig",
"DipoleMeasurementTask",
"DipoleAnalysis",
"DipoleDeblender",
37 "SourceFlagChecker",
"ClassificationDipoleConfig",
"ClassificationDipolePlugin")
41 """Configuration for classification of detected diaSources as dipole or not"""
42 minSn = pexConfig.Field(
43 doc=
"Minimum quadrature sum of positive+negative lobe S/N to be considered a dipole",
44 dtype=float, default=np.sqrt(2) * 5.0,
46 maxFluxRatio = pexConfig.Field(
47 doc=
"Maximum flux ratio in either lobe to be considered a dipole",
48 dtype=float, default=0.65
52 @
register(
"ip_diffim_ClassificationDipole")
54 """A plugin to classify whether a diaSource is a dipole.
57 ConfigClass = ClassificationDipoleConfig
61 return cls.APCORR_ORDER
63 def __init__(self, config, name, schema, metadata):
64 SingleFramePlugin.__init__(self, config, name, schema, metadata)
67 doc=
"Set to 1 for dipoles, else 0.")
68 self.
keyFlag = schema.addField(name +
"_flag", type=
"Flag", doc=
"Set to 1 for any fatal failure.")
71 passesSn = self.dipoleAnalysis.getSn(measRecord) > self.config.minSn
72 negFlux = np.abs(measRecord.get(
"ip_diffim_PsfDipoleFlux_neg_flux"))
73 negFluxFlag = measRecord.get(
"ip_diffim_PsfDipoleFlux_neg_flag")
74 posFlux = np.abs(measRecord.get(
"ip_diffim_PsfDipoleFlux_pos_flux"))
75 posFluxFlag = measRecord.get(
"ip_diffim_PsfDipoleFlux_pos_flag")
77 if negFluxFlag
or posFluxFlag:
81 totalFlux = negFlux + posFlux
84 passesFluxNeg = (negFlux / totalFlux) < self.config.maxFluxRatio
85 passesFluxPos = (posFlux / totalFlux) < self.config.maxFluxRatio
86 if (passesSn
and passesFluxPos
and passesFluxNeg):
93 def fail(self, measRecord, error=None):
94 measRecord.set(self.
keyFlag,
True)
98 """!Measurement of detected diaSources as dipoles"""
101 SingleFrameMeasurementConfig.setDefaults(self)
106 "ip_diffim_NaiveDipoleCentroid",
107 "ip_diffim_NaiveDipoleFlux",
108 "ip_diffim_PsfDipoleFlux",
109 "ip_diffim_ClassificationDipole",
112 self.slots.calibFlux =
None
113 self.slots.modelFlux =
None
114 self.slots.instFlux =
None
115 self.slots.shape =
None
116 self.slots.centroid =
"ip_diffim_NaiveDipoleCentroid"
129 \anchor DipoleMeasurementTask_
131 \brief Measurement of Sources, specifically ones from difference images, for characterization as dipoles
133 \section ip_diffim_dipolemeas_Contents Contents
135 - \ref ip_diffim_dipolemeas_Purpose
136 - \ref ip_diffim_dipolemeas_Initialize
137 - \ref ip_diffim_dipolemeas_IO
138 - \ref ip_diffim_dipolemeas_Config
139 - \ref ip_diffim_dipolemeas_Metadata
140 - \ref ip_diffim_dipolemeas_Debug
141 - \ref ip_diffim_dipolemeas_Example
143 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
145 \section ip_diffim_dipolemeas_Purpose Description
147 This class provides a default configuration for running Source measurement on image differences.
149 These default plugins include:
150 \dontinclude dipoleMeasurement.py
151 \skip class DipoleMeasurementConfig
152 @until self.doReplaceWithNoise
154 These plugins enabled by default allow the user to test the hypothesis that the Source is a dipole.
155 This includes a set of measurements derived from intermediate base classes
156 DipoleCentroidAlgorithm and DipoleFluxAlgorithm. Their respective algorithm control classes are defined in
157 DipoleCentroidControl and DipoleFluxControl. Each centroid and flux measurement will have _neg (negative)
158 and _pos (positive lobe) fields.
160 The first set of measurements uses a "naive" alrogithm for centroid and flux measurements, implemented in
161 NaiveDipoleCentroidControl and NaiveDipoleFluxControl. The algorithm uses a naive 3x3 weighted moment around
162 the nominal centroids of each peak in the Source Footprint. These algorithms fill the table fields
163 ip_diffim_NaiveDipoleCentroid* and ip_diffim_NaiveDipoleFlux*
165 The second set of measurements undertakes a joint-Psf model on the negative and positive lobe simultaneously.
166 This fit simultaneously solves for the negative and positive lobe centroids and fluxes using non-linear
167 least squares minimization. The fields are stored in table elements ip_diffim_PsfDipoleFlux*.
169 Because this Task is just a config for SourceMeasurementTask, the same result may be acheived by manually
170 editing the config and running SourceMeasurementTask. For example:
173 config = SingleFrameMeasurementConfig()
174 config.plugins.names = ["base_PsfFlux",
175 "ip_diffim_PsfDipoleFlux",
176 "ip_diffim_NaiveDipoleFlux",
177 "ip_diffim_NaiveDipoleCentroid",
178 "ip_diffim_ClassificationDipole",
179 "base_CircularApertureFlux",
182 config.slots.calibFlux = None
183 config.slots.modelFlux = None
184 config.slots.instFlux = None
185 config.slots.shape = None
186 config.slots.centroid = "ip_diffim_NaiveDipoleCentroid"
187 config.doReplaceWithNoise = False
189 schema = afwTable.SourceTable.makeMinimalSchema()
190 task = SingleFrameMeasurementTask(schema, config=config)
192 task.run(sources, exposure)
196 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
198 \section ip_diffim_dipolemeas_Initialize Task initialization
200 \copydoc \_\_init\_\_
202 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
204 \section ip_diffim_dipolemeas_IO Invoking the Task
208 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
210 \section ip_diffim_dipolemeas_Config Configuration parameters
212 See \ref DipoleMeasurementConfig
214 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
216 \section ip_diffim_dipolemeas_Metadata Quantities set in Metadata
218 No specific values are set in the Task metadata. However, the Source schema are modified to store the
219 results of the dipole-specific measurements.
222 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
224 \section ip_diffim_dipolemeas_Debug Debug variables
226 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
227 flag \c -d/--debug to import \b debug.py from your \c PYTHONPATH. The relevant contents of debug.py
228 for this Task include:
234 di = lsstDebug.getInfo(name)
235 if name == "lsst.ip.diffim.dipoleMeasurement":
236 di.display = True # enable debug output
237 di.maskTransparency = 90 # ds9 mask transparency
238 di.displayDiaSources = True # show exposure with dipole results
240 lsstDebug.Info = DebugInfo
244 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
246 \section ip_diffim_dipolemeas_Example A complete example of using DipoleMeasurementTask
248 This code is dipoleMeasTask.py in the examples directory, and can be run as \em e.g.
250 examples/dipoleMeasTask.py
251 examples/dipoleMeasTask.py --debug
252 examples/dipoleMeasTask.py --debug --image /path/to/image.fits
255 \dontinclude dipoleMeasTask.py
256 Start the processing by parsing the command line, where the user has the option of enabling debugging output
257 and/or sending their own image for demonstration (in case they have not downloaded the afwdata package).
261 \dontinclude dipoleMeasTask.py
262 The processing occurs in the run function. We first extract an exposure from disk or afwdata, displaying
267 Create a default source schema that we will append fields to as we add more algorithms:
268 \skip makeMinimalSchema
269 @until makeMinimalSchema
271 Create the detection and measurement Tasks, with some minor tweaking of their configs:
275 Having fully initialied the schema, we create a Source table from it:
283 Because we are looking for dipoles, we need to merge the positive and negative detections:
287 Finally, perform measurement (both standard and dipole-specialized) on the merged sources:
291 Optionally display debugging information:
293 @until displayDipoles
294 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
297 ConfigClass = DipoleMeasurementConfig
298 _DefaultName =
"dipoleMeasurement"
306 """!Functor class to check whether a diaSource has flags set that should cause it to be labeled bad."""
311 @param sources Sources that will be measured
312 @param badFlags A list of flags that will be used to determine if there was a measurement problem
314 The list of badFlags will be used to make a list of keys to check for measurement flags on. By
315 default the centroid keys are added to this list"""
317 self.
badFlags = [
'base_PixelFlags_flag_edge',
'base_PixelFlags_flag_interpolatedCenter',
318 'base_PixelFlags_flag_saturatedCenter']
319 if badFlags
is not None:
320 for flag
in badFlags:
321 self.badFlags.append(flag)
322 self.
keys = [sources.getSchema().find(name).key
for name
in self.
badFlags]
323 self.keys.append(sources.table.getCentroidFlagKey())
326 """!Call the source flag checker on a single Source
328 @param source Source that will be examined"""
336 """!Functor class that provides (S/N, position, orientation) of measured dipoles"""
343 """!Parse information returned from dipole measurement
345 @param source The source that will be examined"""
346 return self.getSn(source), self.getCentroid(source), self.getOrientation(source)
349 """!Get the total signal-to-noise of the dipole; total S/N is from positive and negative lobe
351 @param source The source that will be examined"""
353 posflux = source.get(
"ip_diffim_PsfDipoleFlux_pos_flux")
354 posfluxErr = source.get(
"ip_diffim_PsfDipoleFlux_pos_fluxSigma")
355 negflux = source.get(
"ip_diffim_PsfDipoleFlux_neg_flux")
356 negfluxErr = source.get(
"ip_diffim_PsfDipoleFlux_neg_fluxSigma")
359 if (posflux < 0)
is (negflux < 0):
362 return np.sqrt((posflux/posfluxErr)**2 + (negflux/negfluxErr)**2)
365 """!Get the centroid of the dipole; average of positive and negative lobe
367 @param source The source that will be examined"""
369 negCenX = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_x")
370 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
371 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
372 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
373 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
377 0.5*(negCenY+posCenY))
381 """!Calculate the orientation of dipole; vector from negative to positive lobe
383 @param source The source that will be examined"""
385 negCenX = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_x")
386 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
387 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
388 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
389 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
392 dx, dy = posCenX-negCenX, posCenY-negCenY
397 """!Display debugging information on the detected dipoles
399 @param exposure Image the dipoles were measured on
400 @param sources The set of diaSources that were measured"""
406 if not maskTransparency:
407 maskTransparency = 90
408 ds9.setMaskTransparency(maskTransparency)
409 ds9.mtv(exposure, frame=lsstDebug.frame)
411 if display
and displayDiaSources:
412 with ds9.Buffering():
413 for source
in sources:
414 cenX, cenY = source.get(
"ipdiffim_DipolePsfFlux_centroid")
415 if np.isinf(cenX)
or np.isinf(cenY):
416 cenX, cenY = source.getCentroid()
418 isdipole = source.get(
"classification.dipole")
419 if isdipole
and np.isfinite(isdipole):
426 ds9.dot(
"o", cenX, cenY, size=2, ctype=ctype, frame=lsstDebug.frame)
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 ds9.line([(negCenX, negCenY), (posCenX, posCenY)], ctype=
"yellow", frame=lsstDebug.frame)
441 """!Functor to deblend a source as a dipole, and return a new source with deblended footprints.
443 This necessarily overrides some of the functionality from
444 meas_algorithms/python/lsst/meas/algorithms/deblend.py since we
445 need a single source that contains the blended peaks, not
446 multiple children sources. This directly calls the core
447 deblending code deblendBaseline.deblend (optionally _fitPsf for
450 Not actively being used, but there is a unit test for it in
459 self.
log = Log.getLogger(
'ip.diffim.DipoleDeblender')
463 fp = source.getFootprint()
464 peaks = fp.getPeaks()
465 peaksF = [pk.getF()
for pk
in peaks]
467 fmask = afwImage.MaskU(fbb)
468 fmask.setXY0(fbb.getMinX(), fbb.getMinY())
471 psf = exposure.getPsf()
472 psfSigPix = psf.computeShape().getDeterminantRadius()
474 subimage = afwImage.ExposureF(exposure, fbb,
True)
475 cpsf = deblendBaseline.CachingPsf(psf)
479 return source.getTable().copyRecord(source)
482 speaks = [(p.getPeakValue(), p)
for p
in peaks]
484 dpeaks = [speaks[0][1], speaks[-1][1]]
493 fpres = deblendBaseline.deblend(fp, exposure.getMaskedImage(), psf, psfFwhmPix,
502 fpres = deblendBaseline.DeblenderResult(fp, exposure.getMaskedImage(), psf, psfFwhmPix, self.
log)
504 for pki, (pk, pkres, pkF)
in enumerate(zip(dpeaks, fpres.deblendedParents[0].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(),
512 deblendedSource = source.getTable().copyRecord(source)
513 deblendedSource.setParent(source.getId())
514 peakList = deblendedSource.getFootprint().getPeaks()
517 for i, peak
in enumerate(fpres.deblendedParents[0].peaks):
518 if peak.psfFitFlux > 0:
522 c = peak.psfFitCenter
523 self.log.info(
"deblended.centroid.dipole.psf.%s %f %f",
525 self.log.info(
"deblended.chi2dof.dipole.%s %f",
526 suffix, peak.psfFitChisq / peak.psfFitDof)
527 self.log.info(
"deblended.flux.dipole.psf.%s %f",
528 suffix, peak.psfFitFlux * np.sum(peak.templateImage.getArray()))
529 peakList.append(peak.peak)
530 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.
A class representing an Angle.
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.