30 import lsst.meas.deblender.baseline
as deblendBaseline
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:
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]
468 fmask.setXY0(fbb.getMinX(), fbb.getMinY())
469 fp.spans.setMask(fmask, 1)
471 psf = exposure.getPsf()
472 psfSigPix = psf.computeShape().getDeterminantRadius()
474 subimage = afwImage.ExposureF(exposure, bbox=fbb, deep=
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 __call__(self, source, exposure)
def displayDipoles(self, exposure, sources)
Display debugging information on the detected dipoles.
def getCentroid(self, source)
Get the centroid of the dipole; average of positive and negative lobe.
def __call__(self, source)
Call the source flag checker on a single Source.
def fail(self, measRecord, error=None)
def __init__(self, sources, badFlags=None)
Constructor.
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.
Functor class to check whether a diaSource has flags set that should cause it to be labeled bad...
def __init__(self, config, name, schema, metadata)
Measurement of detected diaSources as dipoles.
def getOrientation(self, source)
Calculate the orientation of dipole; vector from negative to positive lobe.
def __call__(self, source)
Parse information returned from dipole measurement.
def getSn(self, source)
Get the total signal-to-noise of the dipole; total S/N is from positive and negative lobe...
def __init__(self)
Constructor.
def getExecutionOrder(cls)
def measure(self, measRecord, exposure)