30 from lsst.meas.base import SingleFrameMeasurementTask, SingleFrameMeasurementConfig
33 __all__ = (
"DipoleMeasurementConfig",
"DipoleMeasurementTask",
"DipoleAnalysis",
"DipoleDeblender")
37 """!Classification of detected diaSources as dipole or not"""
38 minSn = pexConfig.Field(
39 doc=
"Minimum quadrature sum of positive+negative lobe S/N to be considered a dipole",
40 dtype=float, default=np.sqrt(2) * 5.0,
42 maxFluxRatio = pexConfig.Field(
43 doc =
"Maximum flux ratio in either lobe to be considered a dipole",
44 dtype = float, default = 0.65
48 """!Measurement of detected diaSources as dipoles"""
49 classification = pexConfig.ConfigField(
50 dtype=DipoleClassificationConfig,
51 doc=
"Dipole classification config"
56 self.
plugins = [
"ipdiffim_NaiveDipoleCentroid",
57 "ipdiffim_NaiveDipoleFlux",
58 "ipdiffim_PsfDipoleFlux"
70 \anchor DipoleMeasurementTask_
72 \brief Measurement of Sources, specifically ones from difference images, for characterization as dipoles
74 \section ip_diffim_dipolemeas_Contents Contents
76 - \ref ip_diffim_dipolemeas_Purpose
77 - \ref ip_diffim_dipolemeas_Initialize
78 - \ref ip_diffim_dipolemeas_IO
79 - \ref ip_diffim_dipolemeas_Config
80 - \ref ip_diffim_dipolemeas_Metadata
81 - \ref ip_diffim_dipolemeas_Debug
82 - \ref ip_diffim_dipolemeas_Example
84 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
86 \section ip_diffim_dipolemeas_Purpose Description
88 This class provies a specialized set of Source measurements that allow the user to test the hypothesis that
89 the Source is a dipole. This includes a set of measurements derived from intermediate base classes
90 DipoleCentroidAlgorithm and DipoleFluxAlgorithm. Their respective algorithm control classes are defined in
91 DipoleCentroidControl and DipoleFluxControl. Each centroid and flux measurement will have .neg (negative)
92 and .pos (positive lobe) fields.
94 The first set of measurements uses a "naive" alrogithm for centroid and flux measurements, implemented in
95 NaiveDipoleCentroidControl and NaiveDipoleFluxControl. The algorithm uses a naive 3x3 weighted moment around
96 the nominal centroids of each peak in the Source Footprint. These algorithms fill the table fields
97 centroid.dipole.naive.* and flux.dipole.naive.*.
99 The second set of measurements undertakes a joint-Psf model on the negative and positive lobe simultaneously.
100 This fit simultaneously solves for the negative and positive lobe centroids and fluxes using non-linear
101 least squares minimization. The fields are stored in table elements flux.dipole.psf.*.
103 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
105 \section ip_diffim_dipolemeas_Initialize Task initialization
107 \copydoc \_\_init\_\_
109 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
111 \section ip_diffim_dipolemeas_IO Invoking the Task
115 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
117 \section ip_diffim_dipolemeas_Config Configuration parameters
119 See \ref DipoleMeasurementConfig and \ref DipoleClassificationConfig
121 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
123 \section ip_diffim_dipolemeas_Metadata Quantities set in Metadata
125 No specific values are set in the Task metadata. However, the Source schema are modified to store the
126 results of the dipole-specific measurements. These additional fields include:
128 . classification.dipole : probability of being a dipole
130 . centroid.dipole.naive.pos : unweighted 3x3 first moment centroid
132 . centroid.dipole.naive.pos.err : covariance matrix for centroid.dipole.naive.pos
134 . centroid.dipole.naive.pos.flags : set if the centroid.dipole.naive.pos measurement did not fully succeed
136 . centroid.dipole.naive.neg : unweighted 3x3 first moment centroid
138 . centroid.dipole.naive.neg.err : covariance matrix for centroid.dipole.naive.neg
140 . centroid.dipole.naive.neg.flags : set if the centroid.dipole.naive.neg measurement did not fully succeed
142 . flux.dipole.naive.pos : raw flux counts
144 . flux.dipole.naive.pos.err : uncertainty for flux.dipole.naive.pos
146 . flux.dipole.naive.pos.flags : set if the flux.dipole.naive.pos measurement failed
148 . flux.dipole.naive.neg : raw flux counts
150 . flux.dipole.naive.neg.err : uncertainty for flux.dipole.naive.neg
152 . flux.dipole.naive.neg.flags : set if the flux.dipole.naive.neg measurement failed
154 . flux.dipole.naive.npos : number of positive pixels
156 . flux.dipole.naive.nneg : number of negative pixels
158 . flux.dipole.psf.pos : jointly fitted psf flux counts
160 . flux.dipole.psf.pos.err : uncertainty for flux.dipole.psf.pos
162 . flux.dipole.psf.pos.flags : set if the flux.dipole.psf.pos measurement failed
164 . flux.dipole.psf.neg : jointly fitted psf flux counts
166 . flux.dipole.psf.neg.err : uncertainty for flux.dipole.psf.neg
168 . flux.dipole.psf.neg.flags : set if the flux.dipole.psf.neg measurement failed
170 . flux.dipole.psf.chi2dof : chi2 per degree of freedom of fit
172 . flux.dipole.psf.centroid : average of the postive and negative lobe positions
174 . flux.dipole.psf.centroid.err : covariance matrix for flux.dipole.psf.centroid
176 . flux.dipole.psf.centroid.flags : set if the flux.dipole.psf.centroid measurement did not fully succeed
178 . flux.dipole.psf.neg.centroid : psf fitted center of negative lobe
180 . flux.dipole.psf.neg.centroid.err : covariance matrix for flux.dipole.psf.neg.centroid
182 . flux.dipole.psf.neg.centroid.flags : set if the flux.dipole.psf.neg.centroid measurement did not fully succeed
184 . flux.dipole.psf.pos.centroid : psf fitted center of positive lobe
186 . flux.dipole.psf.pos.centroid.err : covariance matrix for flux.dipole.psf.pos.centroid
188 . flux.dipole.psf.pos.centroid.flags : set if the flux.dipole.psf.pos.centroid measurement did not fully succeed
190 . flux.dipole.psf.flags.maxpix : set if too large a footprint was sent to the algorithm
192 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
194 \section ip_diffim_dipolemeas_Debug Debug variables
196 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
197 flag \c -d/--debug to import \b debug.py from your \c PYTHONPATH. The relevant contents of debug.py
198 for this Task include:
204 di = lsstDebug.getInfo(name)
205 if name == "lsst.ip.diffim.dipoleMeasurement":
206 di.display = True # enable debug output
207 di.maskTransparency = 90 # ds9 mask transparency
208 di.displayDiaSources = True # show exposure with dipole results
210 lsstDebug.Info = DebugInfo
214 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
216 \section ip_diffim_dipolemeas_Example A complete example of using DipoleMeasurementTask
218 This code is dipoleMeasTask.py in the examples directory, and can be run as \em e.g.
220 examples/dipoleMeasTask.py
221 examples/dipoleMeasTask.py --debug
222 examples/dipoleMeasTask.py --debug --image /path/to/image.fits
225 \dontinclude dipoleMeasTask.py
226 Start the processing by parsing the command line, where the user has the option of enabling debugging output
227 and/or sending their own image for demonstration (in case they have not downloaded the afwdata package).
231 \dontinclude dipoleMeasTask.py
232 The processing occurs in the run function. We first extract an exposure from disk or afwdata, displaying
237 Create a default source schema that we will append fields to as we add more algorithms:
238 \skip makeMinimalSchema
239 \until makeMinimalSchema
241 Create the detection and measurement Tasks, with some minor tweaking of their configs:
245 Having fully initialied the schema, we create a Source table from it:
253 Because we are looking for dipoles, we need to merge the positive and negative detections:
257 Finally, perform measurement (both standard and dipole-specialized) on the merged sources:
261 Optionally display debugging information:
263 \until displayDipoles
264 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
267 ConfigClass = DipoleMeasurementConfig
268 _DefaultName =
"dipoleMeasurement"
269 _ClassificationFlag =
"classification_dipole"
271 def __init__(self, schema, algMetadata=None, **kwds):
272 """!Create the Task, and add Task-specific fields to the provided measurement table schema.
274 @param[in,out] schema Schema object for measurement fields; modified in-place.
275 @param[in,out] algMetadata Passed to MeasureSources object to be filled with
276 metadata by algorithms (e.g. radii for aperture photometry).
277 @param **kwds Passed to Task.__init__.
279 SingleFrameMeasurementTask.__init__(self, schema, algMetadata, **kwds)
284 """!Create the records needed for dipole classification, and run classifier
286 @param[in,out] sources The diaSources to run classification on; modified in in-place
289 self.log.log(self.log.INFO,
"Classifying %d sources" % len(sources))
293 ctrl = self.config.classification
300 for source
in sources:
301 passesSn = self.dipoleAnalysis.getSn(source) > ctrl.minSn
303 negFlux = np.abs(source.get(
"ip_diffim_PsfDipoleFlux_neg_flux"))
304 posFlux = np.abs(source.get(
"ip_diffim_PsfDipoleFlux_pos_flux"))
305 totalFlux = negFlux + posFlux
306 passesFluxNeg = (negFlux / totalFlux) < ctrl.maxFluxRatio
307 passesFluxPos = (posFlux / totalFlux) < ctrl.maxFluxRatio
308 if (passesSn
and passesFluxPos
and passesFluxNeg):
315 def run(self, sources, exposure, **kwds):
316 """!Run dipole measurement and classification
317 @param sources diaSources that will be measured using dipole measurement
318 @param exposure Exposure on which the diaSources were detected
319 @param **kwds Sent to SingleFrameMeasurementTask
322 SingleFrameMeasurementTask.run(self, sources, exposure, **kwds)
330 """!Functor class to check whether a diaSource has flags set that should cause it to be labeled bad."""
334 @param sources Sources that will be measured
335 @param badFlags A list of flags that will be used to determine if there was a measurement problem
337 The list of badFlags will be used to make a list of keys to check for measurement flags on. By
338 default the centroid keys are added to this list"""
340 self.
badFlags = [
'base_PixelFlags_flag_edge',
'base_PixelFlags_flag_interpolatedCenter',
'base_PixelFlags_flag_saturatedCenter']
341 if badFlags
is not None:
342 for flag
in badFlags:
343 self.badFlags.append(flag)
344 self.
keys = [sources.getSchema().find(name).key
for name
in self.
badFlags]
345 self.keys.append(sources.table.getCentroidFlagKey())
348 """!Call the source flag checker on a single Source
350 @param source Source that will be examined"""
357 """!Functor class that provides (S/N, position, orientation) of measured dipoles"""
363 """!Parse information returned from dipole measurement
365 @param source The source that will be examined"""
366 return self.getSn(source), self.getCentroid(source), self.getOrientation(source)
369 """!Get the total signal-to-noise of the dipole; total S/N is from positive and negative lobe
371 @param source The source that will be examined"""
373 posflux = source.get(
"ip_diffim_PsfDipoleFlux_pos_flux")
374 posfluxErr = source.get(
"ip_diffim_PsfDipoleFlux_pos_fluxSigma")
375 negflux = source.get(
"ip_diffim_PsfDipoleFlux_neg_flux")
376 negfluxErr = source.get(
"ip_diffim_PsfDipoleFlux_neg_fluxSigma")
379 if (posflux < 0)
is (negflux < 0):
382 return np.sqrt((posflux/posfluxErr)**2 + (negflux/negfluxErr)**2)
385 """!Get the centroid of the dipole; average of positive and negative lobe
387 @param source The source that will be examined"""
389 negCenX = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_x")
390 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
391 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
392 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
393 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
397 0.5*(negCenY+posCenY))
401 """!Calculate the orientation of dipole; vector from negative to positive lobe
403 @param source The source that will be examined"""
405 negCenX = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_x")
406 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
407 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
408 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
409 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
412 dx, dy = posCenX-negCenX, posCenY-negCenY
417 """!Display debugging information on the detected dipoles
419 @param exposure Image the dipoles were measured on
420 @param sources The set of diaSources that were measured"""
426 if not maskTransparency:
427 maskTransparency = 90
428 ds9.setMaskTransparency(maskTransparency)
429 ds9.mtv(exposure, frame=lsstDebug.frame)
431 if display
and displayDiaSources:
432 with ds9.Buffering():
433 for source
in sources:
434 cenX, cenY = source.get(
"ipdiffim_DipolePsfFlux_centroid")
435 if np.isinf(cenX)
or np.isinf(cenY):
436 cenX, cenY = source.getCentroid()
438 isdipole = source.get(
"classification.dipole")
439 if isdipole
and np.isfinite(isdipole):
446 ds9.dot(
"o", cenX, cenY, size=2, ctype=ctype, frame=lsstDebug.frame)
448 negCenX = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_x")
449 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
450 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
451 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
452 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
455 ds9.line([(negCenX, negCenY), (posCenX, posCenY)], ctype=
"yellow", frame=lsstDebug.frame)
462 """!Functor to deblend a source as a dipole, and return a new source with deblended footprints.
464 This necessarily overrides some of the functionality from
465 meas_algorithms/python/lsst/meas/algorithms/deblend.py since we
466 need a single source that contains the blended peaks, not
467 multiple children sources. This directly calls the core
468 deblending code deblendBaseline.deblend (optionally _fitPsf for
471 Not actively being used, but there is a unit test for it in
480 'lsst.ip.diffim.DipoleDeblender', pexLog.Log.INFO)
484 fp = source.getFootprint()
485 peaks = fp.getPeaks()
486 peaksF = [pk.getF()
for pk
in peaks]
488 fmask = afwImage.MaskU(fbb)
489 fmask.setXY0(fbb.getMinX(), fbb.getMinY())
492 psf = exposure.getPsf()
493 psfSigPix = psf.computeShape().getDeterminantRadius()
495 subimage = afwImage.ExposureF(exposure, fbb,
True)
496 cpsf = deblendBaseline.CachingPsf(psf)
500 return source.getTable().copyRecord(source)
503 speaks = [(p.getPeakValue(), p)
for p
in peaks]
505 dpeaks = [speaks[0][1], speaks[-1][1]]
510 peaks.push_back(peak)
514 fpres = deblendBaseline.deblend(fp, exposure.getMaskedImage(), psf, psfFwhmPix,
523 fpres = deblendBaseline.PerFootprint()
525 for pki,pk
in enumerate(dpeaks):
526 pkres = deblendBaseline.PerPeak()
529 fpres.peaks.append(pkres)
531 for pki,(pk,pkres,pkF)
in enumerate(zip(dpeaks, fpres.peaks, peaksF)):
532 self.log.logdebug(
'Peak %i' % pki)
533 deblendBaseline._fitPsf(fp, fmask, pk, pkF, pkres, fbb, dpeaks, peaksF, self.
log,
535 subimage.getMaskedImage().getImage(),
536 subimage.getMaskedImage().getVariance(),
540 deblendedSource = source.getTable().copyRecord(source)
541 deblendedSource.setParent(source.getId())
542 peakList = deblendedSource.getFootprint().getPeaks()
545 for i, peak
in enumerate(fpres.peaks):
546 if peak.psfFitFlux > 0:
550 c = peak.psfFitCenter
551 self.log.info(
"deblended.centroid.dipole.psf.%s %f %f" % (
553 self.log.info(
"deblended.chi2dof.dipole.%s %f" % (
554 suffix, peak.psfFitChisq / peak.psfFitDof))
555 self.log.info(
"deblended.flux.dipole.psf.%s %f" % (
556 suffix, peak.psfFitFlux * np.sum(peak.templateMaskedImage.getImage().getArray())))
557 peakList.push_back(peak.peak)
558 return deblendedSource
def getCentroid
Get the centroid of the dipole; average of positive and negative lobe.
string _ClassificationFlag
Classification of detected diaSources as dipole or not.
def displayDipoles
Display debugging information on the detected dipoles.
def classify
Create the records needed for dipole classification, and run classifier.
a place to record messages and descriptions of the state of processing.
def run
Run dipole measurement and classification.
Measurement of Sources, specifically ones from difference images, for characterization as dipoles...
def __init__
Create the Task, and add Task-specific fields to the provided measurement table schema.
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.