22 from __future__
import absolute_import, division, print_function
24 from builtins
import zip
25 from builtins
import object
33 import lsst.meas.deblender.baseline
as deblendBaseline
35 from lsst.meas.base import SingleFrameMeasurementTask, SingleFrameMeasurementConfig, \
36 SingleFramePluginConfig, SingleFramePlugin
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:
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)):
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
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""" 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]
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 __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)