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
66 return cls.APCORR_ORDER
68 def __init__(self, config, name, schema, metadata):
69 SingleFramePlugin.__init__(self, config, name, schema, metadata)
72 doc=
"Set to 1 for dipoles, else 0.")
73 self.
keyFlag = schema.addField(name +
"_flag", type=
"Flag", doc=
"Set to 1 for any fatal failure.")
76 passesSn = self.
dipoleAnalysis.getSn(measRecord) > self.config.minSn
77 negFlux = np.abs(measRecord.get(
"ip_diffim_PsfDipoleFlux_neg_instFlux"))
78 negFluxFlag = measRecord.get(
"ip_diffim_PsfDipoleFlux_neg_flag")
79 posFlux = np.abs(measRecord.get(
"ip_diffim_PsfDipoleFlux_pos_instFlux"))
80 posFluxFlag = measRecord.get(
"ip_diffim_PsfDipoleFlux_pos_flag")
82 if negFluxFlag
or posFluxFlag:
86 totalFlux = negFlux + posFlux
89 passesFluxNeg = (negFlux / totalFlux) < self.config.maxFluxRatio
90 passesFluxPos = (posFlux / totalFlux) < self.config.maxFluxRatio
91 if (passesSn
and passesFluxPos
and passesFluxNeg):
98 def fail(self, measRecord, error=None):
99 measRecord.set(self.
keyFlag,
True)
103 """Measurement of detected diaSources as dipoles""" 106 SingleFrameMeasurementConfig.setDefaults(self)
111 "ip_diffim_NaiveDipoleCentroid",
112 "ip_diffim_NaiveDipoleFlux",
113 "ip_diffim_PsfDipoleFlux",
114 "ip_diffim_ClassificationDipole",
117 self.
slots.calibFlux =
None 118 self.
slots.modelFlux =
None 119 self.
slots.gaussianFlux =
None 120 self.
slots.shape =
None 121 self.
slots.centroid =
"ip_diffim_NaiveDipoleCentroid" 126 """Measurement of Sources, specifically ones from difference images, for characterization as dipoles 130 sources : 'lsst.afw.table.SourceCatalog' 131 Sources that will be measured 132 badFlags : `list` of `dict` 133 A list of flags that will be used to determine if there was a measurement problem 137 The list of badFlags will be used to make a list of keys to check for measurement flags on. By 138 default the centroid keys are added to this list 142 This class provides a default configuration for running Source measurement on image differences. 146 class DipoleMeasurementConfig(SingleFrameMeasurementConfig): 147 "Measurement of detected diaSources as dipoles" 148 def setDefaults(self): 149 SingleFrameMeasurementConfig.setDefaults(self) 150 self.plugins = ["base_CircularApertureFlux", 154 "ip_diffim_NaiveDipoleCentroid", 155 "ip_diffim_NaiveDipoleFlux", 156 "ip_diffim_PsfDipoleFlux", 157 "ip_diffim_ClassificationDipole", 159 self.slots.calibFlux = None 160 self.slots.modelFlux = None 161 self.slots.instFlux = None 162 self.slots.shape = None 163 self.slots.centroid = "ip_diffim_NaiveDipoleCentroid" 164 self.doReplaceWithNoise = False 166 These plugins enabled by default allow the user to test the hypothesis that the Source is a dipole. 167 This includes a set of measurements derived from intermediate base classes 168 DipoleCentroidAlgorithm and DipoleFluxAlgorithm. 169 Their respective algorithm control classes are defined in 170 DipoleCentroidControl and DipoleFluxControl. 171 Each centroid and flux measurement will have _neg (negative) 172 and _pos (positive lobe) fields. 174 The first set of measurements uses a "naive" alrogithm 175 for centroid and flux measurements, implemented in 176 NaiveDipoleCentroidControl and NaiveDipoleFluxControl. 177 The algorithm uses a naive 3x3 weighted moment around 178 the nominal centroids of each peak in the Source Footprint. These algorithms fill the table fields 179 ip_diffim_NaiveDipoleCentroid* and ip_diffim_NaiveDipoleFlux* 181 The second set of measurements undertakes a joint-Psf model on the negative 182 and positive lobe simultaneously. This fit simultaneously solves for the negative and positive 183 lobe centroids and fluxes using non-linear least squares minimization. 184 The fields are stored in table elements ip_diffim_PsfDipoleFlux*. 186 Because this Task is just a config for SourceMeasurementTask, the same result may be acheived by manually 187 editing the config and running SourceMeasurementTask. For example: 191 config = SingleFrameMeasurementConfig() 192 config.plugins.names = ["base_PsfFlux", 193 "ip_diffim_PsfDipoleFlux", 194 "ip_diffim_NaiveDipoleFlux", 195 "ip_diffim_NaiveDipoleCentroid", 196 "ip_diffim_ClassificationDipole", 197 "base_CircularApertureFlux", 200 config.slots.calibFlux = None 201 config.slots.modelFlux = None 202 config.slots.instFlux = None 203 config.slots.shape = None 204 config.slots.centroid = "ip_diffim_NaiveDipoleCentroid" 205 config.doReplaceWithNoise = False 207 schema = afwTable.SourceTable.makeMinimalSchema() 208 task = SingleFrameMeasurementTask(schema, config=config)- 212 The ``lsst.pipe.base.cmdLineTask.CmdLineTask`` command line task interface supports a 213 flag-d/--debug to import debug.py from your PYTHONPATH. The relevant contents of debug.py 214 for this Task include: 221 di = lsstDebug.getInfo(name) 222 if name == "lsst.ip.diffim.dipoleMeasurement": 223 di.display = True # enable debug output 224 di.maskTransparency = 90 # ds9 mask transparency 225 di.displayDiaSources = True # show exposure with dipole results 227 lsstDebug.Info = DebugInfo 230 config.slots.calibFlux = None 231 config.slots.modelFlux = None 232 config.slots.gaussianFlux = None 233 config.slots.shape = None 234 config.slots.centroid = "ip_diffim_NaiveDipoleCentroid" 235 config.doReplaceWithNoise = False 237 This code is dipoleMeasTask.py in the examples directory, and can be run as e.g. 241 examples/dipoleMeasTask.py 242 examples/dipoleMeasTask.py --debug 243 examples/dipoleMeasTask.py --debug --image /path/to/image.fits 247 Start the processing by parsing the command line, where the user has the option of 248 enabling debugging output and/or sending their own image for demonstration 249 (in case they have not downloaded the afwdata package). 253 if __name__ == "__main__": 255 parser = argparse.ArgumentParser( 256 description="Demonstrate the use of SourceDetectionTask and DipoleMeasurementTask") 257 parser.add_argument('--debug', '-d', action="store_true", help="Load debug.py?", default=False) 258 parser.add_argument("--image", "-i", help="User defined image", default=None) 259 args = parser.parse_args() 263 debug.lsstDebug.frame = 2 264 except ImportError as e: 265 print(e, file=sys.stderr) 268 The processing occurs in the run function. We first extract an exposure from disk or afwdata, displaying 274 exposure = loadData(args.image) 276 ds9.mtv(exposure, frame=1) 278 Create a default source schema that we will append fields to as we add more algorithms: 282 schema = afwTable.SourceTable.makeMinimalSchema() 284 Create the detection and measurement Tasks, with some minor tweaking of their configs: 288 # Create the detection task 289 config = SourceDetectionTask.ConfigClass() 290 config.thresholdPolarity = "both" 291 config.background.isNanSafe = True 292 config.thresholdValue = 3 293 detectionTask = SourceDetectionTask(config=config, schema=schema) 294 # And the measurement Task 295 config = DipoleMeasurementTask.ConfigClass() 296 config.plugins.names.remove('base_SkyCoord') 297 algMetadata = dafBase.PropertyList() 298 measureTask = DipoleMeasurementTask(schema, algMetadata, config=config) 300 Having fully initialied the schema, we create a Source table from it: 304 # Create the output table 305 tab = afwTable.SourceTable.make(schema) 312 results = detectionTask.run(tab, exposure) 314 Because we are looking for dipoles, we need to merge the positive and negative detections: 318 # Merge the positve and negative sources 319 fpSet = results.fpSets.positive 321 fpSet.merge(results.fpSets.negative, growFootprint, growFootprint, False) 322 diaSources = afwTable.SourceCatalog(tab) 323 fpSet.makeSources(diaSources) 324 print("Merged %s Sources into %d diaSources (from %d +ve, %d -ve)" % (len(results.sources), 326 results.fpSets.numPos, 327 results.fpSets.numNeg)) 329 Finally, perform measurement (both standard and dipole-specialized) on the merged sources: 333 measureTask.run(diaSources, exposure) 335 Optionally display debugging information: 339 # Display dipoles if debug enabled 341 dpa = DipoleAnalysis() 342 dpa.displayDipoles(exposure, diaSources) 345 ConfigClass = DipoleMeasurementConfig
346 _DefaultName =
"dipoleMeasurement" 354 """Functor class to check whether a diaSource has flags set that should cause it to be labeled bad.""" 357 self.
badFlags = [
'base_PixelFlags_flag_edge',
'base_PixelFlags_flag_interpolatedCenter',
358 'base_PixelFlags_flag_saturatedCenter']
359 if badFlags
is not None:
360 for flag
in badFlags:
362 self.
keys = [sources.getSchema().find(name).key
for name
in self.
badFlags]
363 self.
keys.
append(sources.table.getCentroidFlagKey())
366 """Call the source flag checker on a single Source 371 Source that will be examined 380 """Functor class that provides (S/N, position, orientation) of measured dipoles""" 386 """Parse information returned from dipole measurement 390 source : `lsst.afw.table.SourceRecord` 391 The source that will be examined""" 392 return self.getSn(source), self.getCentroid(source), self.getOrientation(source)
395 """Get the total signal-to-noise of the dipole; total S/N is from positive and negative lobe 399 source : `lsst.afw.table.SourceRecord` 400 The source that will be examined""" 402 posflux = source.get(
"ip_diffim_PsfDipoleFlux_pos_instFlux")
403 posfluxErr = source.get(
"ip_diffim_PsfDipoleFlux_pos_instFluxErr")
404 negflux = source.get(
"ip_diffim_PsfDipoleFlux_neg_instFlux")
405 negfluxErr = source.get(
"ip_diffim_PsfDipoleFlux_neg_instFluxErr")
408 if (posflux < 0)
is (negflux < 0):
411 return np.sqrt((posflux/posfluxErr)**2 + (negflux/negfluxErr)**2)
414 """Get the centroid of the dipole; average of positive and negative lobe 418 source : `lsst.afw.table.SourceRecord` 419 The source that will be examined""" 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)):
429 0.5*(negCenY+posCenY))
433 """Calculate the orientation of dipole; vector from negative to positive lobe 437 source : `lsst.afw.table.SourceRecord` 438 The source that will be examined""" 440 negCenX = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_x")
441 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
442 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
443 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
444 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
447 dx, dy = posCenX-negCenX, posCenY-negCenY
452 """Display debugging information on the detected dipoles 456 exposure : `lsst.afw.image.Exposure` 457 Image the dipoles were measured on 458 sources : `lsst.afw.table.SourceCatalog` 459 The set of diaSources that were measured""" 465 if not maskTransparency:
466 maskTransparency = 90
467 ds9.setMaskTransparency(maskTransparency)
468 ds9.mtv(exposure, frame=lsstDebug.frame)
470 if display
and displayDiaSources:
471 with ds9.Buffering():
472 for source
in sources:
473 cenX, cenY = source.get(
"ipdiffim_DipolePsfFlux_centroid")
474 if np.isinf(cenX)
or np.isinf(cenY):
475 cenX, cenY = source.getCentroid()
477 isdipole = source.get(
"classification.dipole")
478 if isdipole
and np.isfinite(isdipole):
485 ds9.dot(
"o", cenX, cenY, size=2, ctype=ctype, frame=lsstDebug.frame)
487 negCenX = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_x")
488 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
489 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
490 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
491 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
494 ds9.line([(negCenX, negCenY), (posCenX, posCenY)], ctype=
"yellow", frame=lsstDebug.frame)
500 """Functor to deblend a source as a dipole, and return a new source with deblended footprints. 502 This necessarily overrides some of the functionality from 503 meas_algorithms/python/lsst/meas/algorithms/deblend.py since we 504 need a single source that contains the blended peaks, not 505 multiple children sources. This directly calls the core 506 deblending code deblendBaseline.deblend (optionally _fitPsf for 509 Not actively being used, but there is a unit test for it in 518 self.
log = Log.getLogger(
'ip.diffim.DipoleDeblender')
522 fp = source.getFootprint()
523 peaks = fp.getPeaks()
524 peaksF = [pk.getF()
for pk
in peaks]
527 fmask.setXY0(fbb.getMinX(), fbb.getMinY())
528 fp.spans.setMask(fmask, 1)
530 psf = exposure.getPsf()
531 psfSigPix = psf.computeShape().getDeterminantRadius()
533 subimage = afwImage.ExposureF(exposure, bbox=fbb, deep=
True)
534 cpsf = deblendBaseline.CachingPsf(psf)
538 return source.getTable().copyRecord(source)
541 speaks = [(p.getPeakValue(), p)
for p
in peaks]
543 dpeaks = [speaks[0][1], speaks[-1][1]]
552 fpres = deblendBaseline.deblend(fp, exposure.getMaskedImage(), psf, psfFwhmPix,
561 fpres = deblendBaseline.DeblenderResult(fp, exposure.getMaskedImage(), psf, psfFwhmPix, self.
log)
563 for pki, (pk, pkres, pkF)
in enumerate(zip(dpeaks, fpres.deblendedParents[0].peaks, peaksF)):
565 deblendBaseline._fitPsf(fp, fmask, pk, pkF, pkres, fbb, dpeaks, peaksF, self.
log,
567 subimage.getMaskedImage().getImage(),
568 subimage.getMaskedImage().getVariance(),
571 deblendedSource = source.getTable().copyRecord(source)
572 deblendedSource.setParent(source.getId())
573 peakList = deblendedSource.getFootprint().getPeaks()
576 for i, peak
in enumerate(fpres.deblendedParents[0].peaks):
577 if peak.psfFitFlux > 0:
581 c = peak.psfFitCenter
582 self.
log.
info(
"deblended.centroid.dipole.psf.%s %f %f",
584 self.
log.
info(
"deblended.chi2dof.dipole.%s %f",
585 suffix, peak.psfFitChisq / peak.psfFitDof)
586 self.
log.
info(
"deblended.flux.dipole.psf.%s %f",
587 suffix, peak.psfFitFlux * np.sum(peak.templateImage.getArray()))
588 peakList.append(peak.peak)
589 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)
def __call__(self, source)
def getExecutionOrder(cls)
def measure(self, measRecord, exposure)