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
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_instFlux"))
76 negFluxFlag = measRecord.get(
"ip_diffim_PsfDipoleFlux_neg_flag")
77 posFlux = np.abs(measRecord.get(
"ip_diffim_PsfDipoleFlux_pos_instFlux"))
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.gaussianFlux =
None 118 self.
slots.shape =
None 119 self.
slots.centroid =
"ip_diffim_NaiveDipoleCentroid" 124 """Measurement of Sources, specifically ones from difference images, for characterization as dipoles 128 sources : 'lsst.afw.table.SourceCatalog' 129 Sources that will be measured 130 badFlags : `list` of `dict` 131 A list of flags that will be used to determine if there was a measurement problem 135 The list of badFlags will be used to make a list of keys to check for measurement flags on. By 136 default the centroid keys are added to this list 140 This class provides a default configuration for running Source measurement on image differences. 144 class DipoleMeasurementConfig(SingleFrameMeasurementConfig): 145 "Measurement of detected diaSources as dipoles" 146 def setDefaults(self): 147 SingleFrameMeasurementConfig.setDefaults(self) 148 self.plugins = ["base_CircularApertureFlux", 152 "ip_diffim_NaiveDipoleCentroid", 153 "ip_diffim_NaiveDipoleFlux", 154 "ip_diffim_PsfDipoleFlux", 155 "ip_diffim_ClassificationDipole", 157 self.slots.calibFlux = None 158 self.slots.modelFlux = None 159 self.slots.instFlux = None 160 self.slots.shape = None 161 self.slots.centroid = "ip_diffim_NaiveDipoleCentroid" 162 self.doReplaceWithNoise = False 164 These plugins enabled by default allow the user to test the hypothesis that the Source is a dipole. 165 This includes a set of measurements derived from intermediate base classes 166 DipoleCentroidAlgorithm and DipoleFluxAlgorithm. 167 Their respective algorithm control classes are defined in 168 DipoleCentroidControl and DipoleFluxControl. 169 Each centroid and flux measurement will have _neg (negative) 170 and _pos (positive lobe) fields. 172 The first set of measurements uses a "naive" alrogithm 173 for centroid and flux measurements, implemented in 174 NaiveDipoleCentroidControl and NaiveDipoleFluxControl. 175 The algorithm uses a naive 3x3 weighted moment around 176 the nominal centroids of each peak in the Source Footprint. These algorithms fill the table fields 177 ip_diffim_NaiveDipoleCentroid* and ip_diffim_NaiveDipoleFlux* 179 The second set of measurements undertakes a joint-Psf model on the negative 180 and positive lobe simultaneously. This fit simultaneously solves for the negative and positive 181 lobe centroids and fluxes using non-linear least squares minimization. 182 The fields are stored in table elements ip_diffim_PsfDipoleFlux*. 184 Because this Task is just a config for SingleFrameMeasurementTask, the same result may be acheived by 185 manually editing the config and running SingleFrameMeasurementTask. For example: 189 config = SingleFrameMeasurementConfig() 190 config.plugins.names = ["base_PsfFlux", 191 "ip_diffim_PsfDipoleFlux", 192 "ip_diffim_NaiveDipoleFlux", 193 "ip_diffim_NaiveDipoleCentroid", 194 "ip_diffim_ClassificationDipole", 195 "base_CircularApertureFlux", 198 config.slots.calibFlux = None 199 config.slots.modelFlux = None 200 config.slots.instFlux = None 201 config.slots.shape = None 202 config.slots.centroid = "ip_diffim_NaiveDipoleCentroid" 203 config.doReplaceWithNoise = False 205 schema = afwTable.SourceTable.makeMinimalSchema() 206 task = SingleFrameMeasurementTask(schema, config=config)- 210 The ``lsst.pipe.base.cmdLineTask.CmdLineTask`` command line task interface supports a 211 flag-d/--debug to import debug.py from your PYTHONPATH. The relevant contents of debug.py 212 for this Task include: 219 di = lsstDebug.getInfo(name) 220 if name == "lsst.ip.diffim.dipoleMeasurement": 221 di.display = True # enable debug output 222 di.maskTransparency = 90 # display mask transparency 223 di.displayDiaSources = True # show exposure with dipole results 225 lsstDebug.Info = DebugInfo 228 config.slots.calibFlux = None 229 config.slots.modelFlux = None 230 config.slots.gaussianFlux = None 231 config.slots.shape = None 232 config.slots.centroid = "ip_diffim_NaiveDipoleCentroid" 233 config.doReplaceWithNoise = False 235 This code is dipoleMeasTask.py in the examples directory, and can be run as e.g. 239 examples/dipoleMeasTask.py 240 examples/dipoleMeasTask.py --debug 241 examples/dipoleMeasTask.py --debug --image /path/to/image.fits 245 Start the processing by parsing the command line, where the user has the option of 246 enabling debugging output and/or sending their own image for demonstration 247 (in case they have not downloaded the afwdata package). 251 if __name__ == "__main__": 253 parser = argparse.ArgumentParser( 254 description="Demonstrate the use of SourceDetectionTask and DipoleMeasurementTask") 255 parser.add_argument('--debug', '-d', action="store_true", help="Load debug.py?", default=False) 256 parser.add_argument("--image", "-i", help="User defined image", default=None) 257 args = parser.parse_args() 261 debug.lsstDebug.frame = 2 262 except ImportError as e: 263 print(e, file=sys.stderr) 266 The processing occurs in the run function. We first extract an exposure from disk or afwdata, displaying 272 exposure = loadData(args.image) 274 afwDisplay.Display(frame=1).mtv(exposure) 276 Create a default source schema that we will append fields to as we add more algorithms: 280 schema = afwTable.SourceTable.makeMinimalSchema() 282 Create the detection and measurement Tasks, with some minor tweaking of their configs: 286 # Create the detection task 287 config = SourceDetectionTask.ConfigClass() 288 config.thresholdPolarity = "both" 289 config.background.isNanSafe = True 290 config.thresholdValue = 3 291 detectionTask = SourceDetectionTask(config=config, schema=schema) 292 # And the measurement Task 293 config = DipoleMeasurementTask.ConfigClass() 294 config.plugins.names.remove('base_SkyCoord') 295 algMetadata = dafBase.PropertyList() 296 measureTask = DipoleMeasurementTask(schema, algMetadata, config=config) 298 Having fully initialied the schema, we create a Source table from it: 302 # Create the output table 303 tab = afwTable.SourceTable.make(schema) 310 results = detectionTask.run(tab, exposure) 312 Because we are looking for dipoles, we need to merge the positive and negative detections: 316 # Merge the positve and negative sources 317 fpSet = results.fpSets.positive 319 fpSet.merge(results.fpSets.negative, growFootprint, growFootprint, False) 320 diaSources = afwTable.SourceCatalog(tab) 321 fpSet.makeSources(diaSources) 322 print("Merged %s Sources into %d diaSources (from %d +ve, %d -ve)" % (len(results.sources), 324 results.fpSets.numPos, 325 results.fpSets.numNeg)) 327 Finally, perform measurement (both standard and dipole-specialized) on the merged sources: 331 measureTask.run(diaSources, exposure) 333 Optionally display debugging information: 337 # Display dipoles if debug enabled 339 dpa = DipoleAnalysis() 340 dpa.displayDipoles(exposure, diaSources) 343 ConfigClass = DipoleMeasurementConfig
344 _DefaultName =
"dipoleMeasurement" 352 """Functor class to check whether a diaSource has flags set that should cause it to be labeled bad.""" 355 self.
badFlags = [
'base_PixelFlags_flag_edge',
'base_PixelFlags_flag_interpolatedCenter',
356 'base_PixelFlags_flag_saturatedCenter']
357 if badFlags
is not None:
358 for flag
in badFlags:
360 self.
keys = [sources.getSchema().find(name).key
for name
in self.
badFlags]
361 self.
keys.
append(sources.table.getCentroidFlagKey())
364 """Call the source flag checker on a single Source 369 Source that will be examined 378 """Functor class that provides (S/N, position, orientation) of measured dipoles""" 384 """Parse information returned from dipole measurement 388 source : `lsst.afw.table.SourceRecord` 389 The source that will be examined""" 390 return self.getSn(source), self.getCentroid(source), self.getOrientation(source)
393 """Get the total signal-to-noise of the dipole; total S/N is from positive and negative lobe 397 source : `lsst.afw.table.SourceRecord` 398 The source that will be examined""" 400 posflux = source.get(
"ip_diffim_PsfDipoleFlux_pos_instFlux")
401 posfluxErr = source.get(
"ip_diffim_PsfDipoleFlux_pos_instFluxErr")
402 negflux = source.get(
"ip_diffim_PsfDipoleFlux_neg_instFlux")
403 negfluxErr = source.get(
"ip_diffim_PsfDipoleFlux_neg_instFluxErr")
406 if (posflux < 0)
is (negflux < 0):
409 return np.sqrt((posflux/posfluxErr)**2 + (negflux/negfluxErr)**2)
412 """Get the centroid of the dipole; average of positive and negative lobe 416 source : `lsst.afw.table.SourceRecord` 417 The source that will be examined""" 419 negCenX = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_x")
420 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
421 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
422 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
423 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
427 0.5*(negCenY+posCenY))
431 """Calculate the orientation of dipole; vector from negative to positive lobe 435 source : `lsst.afw.table.SourceRecord` 436 The source that will be examined""" 438 negCenX = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_x")
439 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
440 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
441 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
442 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
445 dx, dy = posCenX-negCenX, posCenY-negCenY
450 """Display debugging information on the detected dipoles 454 exposure : `lsst.afw.image.Exposure` 455 Image the dipoles were measured on 456 sources : `lsst.afw.table.SourceCatalog` 457 The set of diaSources that were measured""" 463 if not maskTransparency:
464 maskTransparency = 90
465 disp = afwDisplay.Display(frame=lsstDebug.frame)
466 disp.setMaskTransparency(maskTransparency)
469 if display
and displayDiaSources:
470 with disp.Buffering():
471 for source
in sources:
472 cenX, cenY = source.get(
"ipdiffim_DipolePsfFlux_centroid")
473 if np.isinf(cenX)
or np.isinf(cenY):
474 cenX, cenY = source.getCentroid()
476 isdipole = source.get(
"ip_diffim_ClassificationDipole_value")
477 if isdipole
and np.isfinite(isdipole):
479 ctype = afwDisplay.GREEN
482 ctype = afwDisplay.RED
484 disp.dot(
"o", cenX, cenY, size=2, ctype=ctype)
486 negCenX = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_x")
487 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
488 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
489 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
490 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
493 disp.line([(negCenX, negCenY), (posCenX, posCenY)], ctype=afwDisplay.YELLOW)
499 """Functor to deblend a source as a dipole, and return a new source with deblended footprints. 501 This necessarily overrides some of the functionality from 502 meas_algorithms/python/lsst/meas/algorithms/deblend.py since we 503 need a single source that contains the blended peaks, not 504 multiple children sources. This directly calls the core 505 deblending code deblendBaseline.deblend (optionally _fitPsf for 508 Not actively being used, but there is a unit test for it in 517 self.
log = Log.getLogger(
'ip.diffim.DipoleDeblender')
521 fp = source.getFootprint()
522 peaks = fp.getPeaks()
523 peaksF = [pk.getF()
for pk
in peaks]
526 fmask.setXY0(fbb.getMinX(), fbb.getMinY())
527 fp.spans.setMask(fmask, 1)
529 psf = exposure.getPsf()
530 psfSigPix = psf.computeShape().getDeterminantRadius()
532 subimage = afwImage.ExposureF(exposure, bbox=fbb, deep=
True)
533 cpsf = deblendBaseline.CachingPsf(psf)
537 return source.getTable().copyRecord(source)
540 speaks = [(p.getPeakValue(), p)
for p
in peaks]
542 dpeaks = [speaks[0][1], speaks[-1][1]]
551 fpres = deblendBaseline.deblend(fp, exposure.getMaskedImage(), psf, psfFwhmPix,
560 fpres = deblendBaseline.DeblenderResult(fp, exposure.getMaskedImage(), psf, psfFwhmPix, self.
log)
562 for pki, (pk, pkres, pkF)
in enumerate(zip(dpeaks, fpres.deblendedParents[0].peaks, peaksF)):
564 deblendBaseline._fitPsf(fp, fmask, pk, pkF, pkres, fbb, dpeaks, peaksF, self.
log,
566 subimage.getMaskedImage().getImage(),
567 subimage.getMaskedImage().getVariance(),
570 deblendedSource = source.getTable().copyRecord(source)
571 deblendedSource.setParent(source.getId())
572 peakList = deblendedSource.getFootprint().getPeaks()
575 for i, peak
in enumerate(fpres.deblendedParents[0].peaks):
576 if peak.psfFitFlux > 0:
580 c = peak.psfFitCenter
581 self.
log.
info(
"deblended.centroid.dipole.psf.%s %f %f",
583 self.
log.
info(
"deblended.chi2dof.dipole.%s %f",
584 suffix, peak.psfFitChisq / peak.psfFitDof)
585 self.
log.
info(
"deblended.flux.dipole.psf.%s %f",
586 suffix, peak.psfFitFlux * np.sum(peak.templateImage.getArray()))
587 peakList.append(peak.peak)
588 return deblendedSource
def __call__(self, source, exposure)
def displayDipoles(self, exposure, sources)
def getCentroid(self, source)
def __call__(self, source)
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
A class representing an angle.
def fail(self, measRecord, error=None)
Represent a 2-dimensional array of bitmask pixels.
def __init__(self, sources, badFlags=None)
def __init__(self, config, name, schema, metadata)
def getOrientation(self, source)
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...
def __call__(self, source)
def getExecutionOrder(cls)
def measure(self, measRecord, exposure)