36 """!Classification of detected diaSources as dipole or not"""
37 minSn = pexConfig.Field(
38 doc=
"Minimum quadrature sum of positive+negative lobe S/N to be considered a dipole",
39 dtype=float, default=np.sqrt(2) * 5.0,
41 maxFluxRatio = pexConfig.Field(
42 doc =
"Maximum flux ratio in either lobe to be considered a dipole",
43 dtype = float, default = 0.65
47 """!Measurement of detected diaSources as dipoles"""
48 classification = pexConfig.ConfigField(
49 dtype=DipoleClassificationConfig,
50 doc=
"Dipole classification config"
54 self.algorithms.names.add(
"centroid.dipole.naive")
55 self.algorithms.names.add(
"flux.dipole.naive")
56 self.algorithms.names.add(
"flux.dipole.psf")
67 \anchor DipoleMeasurementTask_
69 \brief Measurement of Sources, specifically ones from difference images, for characterization as dipoles
71 \section ip_diffim_dipolemeas_Contents Contents
73 - \ref ip_diffim_dipolemeas_Purpose
74 - \ref ip_diffim_dipolemeas_Initialize
75 - \ref ip_diffim_dipolemeas_IO
76 - \ref ip_diffim_dipolemeas_Config
77 - \ref ip_diffim_dipolemeas_Metadata
78 - \ref ip_diffim_dipolemeas_Debug
79 - \ref ip_diffim_dipolemeas_Example
81 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
83 \section ip_diffim_dipolemeas_Purpose Description
85 This class provies a specialized set of Source measurements that allow the user to test the hypothesis that
86 the Source is a dipole. This includes a set of measurements derived from intermediate base classes
87 DipoleCentroidAlgorithm and DipoleFluxAlgorithm. Their respective algorithm control classes are defined in
88 DipoleCentroidControl and DipoleFluxControl. Each centroid and flux measurement will have .neg (negative)
89 and .pos (positive lobe) fields.
91 The first set of measurements uses a "naive" alrogithm for centroid and flux measurements, implemented in
92 NaiveDipoleCentroidControl and NaiveDipoleFluxControl. The algorithm uses a naive 3x3 weighted moment around
93 the nominal centroids of each peak in the Source Footprint. These algorithms fill the table fields
94 centroid.dipole.naive.* and flux.dipole.naive.*.
96 The second set of measurements undertakes a joint-Psf model on the negative and positive lobe simultaneously.
97 This fit simultaneously solves for the negative and positive lobe centroids and fluxes using non-linear
98 least squares minimization. The fields are stored in table elements flux.dipole.psf.*.
100 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
102 \section ip_diffim_dipolemeas_Initialize Task initialization
104 \copydoc \_\_init\_\_
106 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
108 \section ip_diffim_dipolemeas_IO Invoking the Task
112 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
114 \section ip_diffim_dipolemeas_Config Configuration parameters
116 See \ref DipoleMeasurementConfig and \ref DipoleClassificationConfig
118 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
120 \section ip_diffim_dipolemeas_Metadata Quantities set in Metadata
122 No specific values are set in the Task metadata. However, the Source schema are modified to store the
123 results of the dipole-specific measurements. These additional fields include:
125 . classification.dipole : probability of being a dipole
127 . centroid.dipole.naive.pos : unweighted 3x3 first moment centroid
129 . centroid.dipole.naive.pos.err : covariance matrix for centroid.dipole.naive.pos
131 . centroid.dipole.naive.pos.flags : set if the centroid.dipole.naive.pos measurement did not fully succeed
133 . centroid.dipole.naive.neg : unweighted 3x3 first moment centroid
135 . centroid.dipole.naive.neg.err : covariance matrix for centroid.dipole.naive.neg
137 . centroid.dipole.naive.neg.flags : set if the centroid.dipole.naive.neg measurement did not fully succeed
139 . flux.dipole.naive.pos : raw flux counts
141 . flux.dipole.naive.pos.err : uncertainty for flux.dipole.naive.pos
143 . flux.dipole.naive.pos.flags : set if the flux.dipole.naive.pos measurement failed
145 . flux.dipole.naive.neg : raw flux counts
147 . flux.dipole.naive.neg.err : uncertainty for flux.dipole.naive.neg
149 . flux.dipole.naive.neg.flags : set if the flux.dipole.naive.neg measurement failed
151 . flux.dipole.naive.npos : number of positive pixels
153 . flux.dipole.naive.nneg : number of negative pixels
155 . flux.dipole.psf.pos : jointly fitted psf flux counts
157 . flux.dipole.psf.pos.err : uncertainty for flux.dipole.psf.pos
159 . flux.dipole.psf.pos.flags : set if the flux.dipole.psf.pos measurement failed
161 . flux.dipole.psf.neg : jointly fitted psf flux counts
163 . flux.dipole.psf.neg.err : uncertainty for flux.dipole.psf.neg
165 . flux.dipole.psf.neg.flags : set if the flux.dipole.psf.neg measurement failed
167 . flux.dipole.psf.chi2dof : chi2 per degree of freedom of fit
169 . flux.dipole.psf.centroid : average of the postive and negative lobe positions
171 . flux.dipole.psf.centroid.err : covariance matrix for flux.dipole.psf.centroid
173 . flux.dipole.psf.centroid.flags : set if the flux.dipole.psf.centroid measurement did not fully succeed
175 . flux.dipole.psf.neg.centroid : psf fitted center of negative lobe
177 . flux.dipole.psf.neg.centroid.err : covariance matrix for flux.dipole.psf.neg.centroid
179 . flux.dipole.psf.neg.centroid.flags : set if the flux.dipole.psf.neg.centroid measurement did not fully succeed
181 . flux.dipole.psf.pos.centroid : psf fitted center of positive lobe
183 . flux.dipole.psf.pos.centroid.err : covariance matrix for flux.dipole.psf.pos.centroid
185 . flux.dipole.psf.pos.centroid.flags : set if the flux.dipole.psf.pos.centroid measurement did not fully succeed
187 . flux.dipole.psf.flags.maxpix : set if too large a footprint was sent to the algorithm
189 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
191 \section ip_diffim_dipolemeas_Debug Debug variables
193 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
194 flag \c -d/--debug to import \b debug.py from your \c PYTHONPATH. The relevant contents of debug.py
195 for this Task include:
201 di = lsstDebug.getInfo(name)
202 if name == "lsst.ip.diffim.dipoleMeasurement":
203 di.display = True # enable debug output
204 di.maskTransparency = 90 # ds9 mask transparency
205 di.displayDiaSources = True # show exposure with dipole results
207 lsstDebug.Info = DebugInfo
211 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
213 \section ip_diffim_dipolemeas_Example A complete example of using DipoleMeasurementTask
215 This code is dipoleMeasTask.py in the examples directory, and can be run as \em e.g.
217 examples/dipoleMeasTask.py
218 examples/dipoleMeasTask.py --debug
219 examples/dipoleMeasTask.py --debug --image /path/to/image.fits
222 \dontinclude dipoleMeasTask.py
223 Start the processing by parsing the command line, where the user has the option of enabling debugging output
224 and/or sending their own image for demonstration (in case they have not downloaded the afwdata package).
228 \dontinclude dipoleMeasTask.py
229 The processing occurs in the run function. We first extract an exposure from disk or afwdata, displaying
234 Create a default source schema that we will append fields to as we add more algorithms:
235 \skip makeMinimalSchema
236 \until makeMinimalSchema
238 Create the detection and measurement Tasks, with some minor tweaking of their configs:
242 Having fully initialied the schema, we create a Source table from it:
250 Because we are looking for dipoles, we need to merge the positive and negative detections:
254 Finally, perform measurement (both standard and dipole-specialized) on the merged sources:
258 Optionally display debugging information:
260 \until displayDipoles
261 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
264 ConfigClass = DipoleMeasurementConfig
265 _DefaultName =
"dipoleMeasurement"
266 _ClassificationFlag =
"classification.dipole"
268 def __init__(self, schema, algMetadata=None, **kwds):
269 """!Create the Task, and add Task-specific fields to the provided measurement table schema.
271 @param[in,out] schema Schema object for measurement fields; modified in-place.
272 @param[in,out] algMetadata Passed to MeasureSources object to be filled with
273 metadata by algorithms (e.g. radii for aperture photometry).
274 @param **kwds Passed to Task.__init__.
276 SourceMeasurementTask.__init__(self, schema, algMetadata, **kwds)
281 """!Create the records needed for dipole classification, and run classifier
283 @param[in,out] sources The diaSources to run classification on; modified in in-place
286 self.log.log(self.log.INFO,
"Classifying %d sources" % len(sources))
290 ctrl = self.config.classification
297 for source
in sources:
298 passesSn = self.dipoleAnalysis.getSn(source) > ctrl.minSn
300 negFlux = np.abs(source.get(
"flux.dipole.psf.neg"))
301 posFlux = np.abs(source.get(
"flux.dipole.psf.pos"))
302 totalFlux = negFlux + posFlux
303 passesFluxNeg = (negFlux / (negFlux + posFlux)) < ctrl.maxFluxRatio
304 passesFluxPos = (posFlux / (negFlux + posFlux)) < ctrl.maxFluxRatio
306 if (passesSn
and passesFluxPos
and passesFluxNeg):
313 def run(self, exposure, sources, **kwds):
314 """!Run dipole measurement and classification
315 @param exposure Exposure on which the diaSources were detected
316 @param sources diaSources that will be measured using dipole measurement
317 @param **kwds Sent to SourceMeasurementTask
320 SourceMeasurementTask.run(self, exposure, sources, **kwds)
328 """!Functor class to check whether a diaSource has flags set that should cause it to be labeled bad."""
332 @param sources Sources that will be measured
333 @param badFlags A list of flags that will be used to determine if there was a measurement problem
335 The list of badFlags will be used to make a list of keys to check for measurement flags on. By
336 default the centroid keys are added to this list"""
338 self.
badFlags = [
'flags.pixel.edge',
'flags.pixel.interpolated.center',
'flags.pixel.saturated.center']
339 if badFlags
is not None:
340 for flag
in badFlags:
341 self.badFlags.append(flag)
342 self.
keys = [sources.getSchema().find(name).key
for name
in self.
badFlags]
343 self.keys.append(sources.table.getCentroidFlagKey())
346 """!Call the source flag checker on a single Source
348 @param source Source that will be examined"""
355 """!Functor class that provides (S/N, position, orientation) of measured dipoles"""
361 """!Parse information returned from dipole measurement
363 @param source The source that will be examined"""
364 return self.getSn(source), self.getCentroid(source), self.getOrientation(source)
367 """!Get the total signal-to-noise of the dipole; total S/N is from positive and negative lobe
369 @param source The source that will be examined"""
371 posflux = source.get(
"flux.dipole.psf.pos")
372 posfluxErr = source.get(
"flux.dipole.psf.pos.err")
373 negflux = source.get(
"flux.dipole.psf.neg")
374 negfluxErr = source.get(
"flux.dipole.psf.neg.err")
377 if (posflux < 0)
is (negflux < 0):
380 return np.sqrt((posflux/posfluxErr)**2 + (negflux/negfluxErr)**2)
383 """!Get the centroid of the dipole; average of positive and negative lobe
385 @param source The source that will be examined"""
387 negCen = source.get(
"flux.dipole.psf.neg.centroid")
388 posCen = source.get(
"flux.dipole.psf.pos.centroid")
389 if (
False in np.isfinite(negCen))
or (
False in np.isfinite(posCen)):
393 0.5*(negCen[1]+posCen[1]))
397 """!Calculate the orientation of dipole; vector from negative to positive lobe
399 @param source The source that will be examined"""
401 negCen = source.get(
"flux.dipole.psf.neg.centroid")
402 posCen = source.get(
"flux.dipole.psf.pos.centroid")
403 if (
False in np.isfinite(negCen))
or (
False in np.isfinite(posCen)):
406 dx, dy = posCen[0]-negCen[0], posCen[1]-negCen[1]
411 """!Display debugging information on the detected dipoles
413 @param exposure Image the dipoles were measured on
414 @param sources The set of diaSources that were measured"""
424 if not maskTransparency:
425 maskTransparency = 90
426 ds9.setMaskTransparency(maskTransparency)
427 ds9.mtv(exposure, frame=lsstDebug.frame)
429 if display
and displayDiaSources:
430 with ds9.Buffering():
431 for source
in sources:
432 cenX, cenY = source.get(
"flux.dipole.psf.centroid")
433 if np.isinf(cenX)
or np.isinf(cenY):
434 cenX, cenY = source.getCentroid()
436 isdipole = source.get(
"classification.dipole")
437 if isdipole
and np.isfinite(isdipole):
444 ds9.dot(
"o", cenX, cenY, size=2, ctype=ctype, frame=lsstDebug.frame)
446 negCenX, negCenY = source.get(
"flux.dipole.psf.neg.centroid")
447 posCenX, posCenY = source.get(
"flux.dipole.psf.pos.centroid")
448 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
451 ds9.line([(negCenX, negCenY), (posCenX, posCenY)], ctype=
"yellow", frame=lsstDebug.frame)
458 """!Functor to deblend a source as a dipole, and return a new source with deblended footprints.
460 This necessarily overrides some of the functionality from
461 meas_algorithms/python/lsst/meas/algorithms/deblend.py since we
462 need a single source that contains the blended peaks, not
463 multiple children sources. This directly calls the core
464 deblending code deblendBaseline.deblend (optionally _fitPsf for
467 Not actively being used, but there is a unit test for it in
476 'lsst.ip.diffim.DipoleDeblender', pexLog.Log.INFO)
480 fp = source.getFootprint()
481 peaks = fp.getPeaks()
482 peaksF = [pk.getF()
for pk
in peaks]
484 fmask = afwImage.MaskU(fbb)
485 fmask.setXY0(fbb.getMinX(), fbb.getMinY())
488 psf = exposure.getPsf()
489 psfSigPix = psf.computeShape().getDeterminantRadius()
491 subimage = afwImage.ExposureF(exposure, fbb,
True)
492 cpsf = deblendBaseline.CachingPsf(psf)
496 return source.getTable().copyRecord(source)
499 speaks = [(p.getPeakValue(), p)
for p
in peaks]
501 dpeaks = [speaks[0][1], speaks[-1][1]]
506 peaks.push_back(peak)
510 fpres = deblendBaseline.deblend(fp, exposure.getMaskedImage(), psf, psfFwhmPix,
519 fpres = deblendBaseline.PerFootprint()
521 for pki,pk
in enumerate(dpeaks):
522 pkres = deblendBaseline.PerPeak()
525 fpres.peaks.append(pkres)
527 for pki,(pk,pkres,pkF)
in enumerate(zip(dpeaks, fpres.peaks, peaksF)):
528 self.log.logdebug(
'Peak %i' % pki)
529 deblendBaseline._fitPsf(fp, fmask, pk, pkF, pkres, fbb, dpeaks, peaksF, self.
log,
531 subimage.getMaskedImage().getImage(),
532 subimage.getMaskedImage().getVariance(),
536 deblendedSource = source.getTable().copyRecord(source)
537 deblendedSource.setParent(source.getId())
538 peakList = deblendedSource.getFootprint().getPeaks()
541 for i, peak
in enumerate(fpres.peaks):
542 if peak.psfFitFlux > 0:
546 c = peak.psfFitCenter
547 self.log.info(
"deblended.centroid.dipole.psf.%s %f %f" % (
549 self.log.info(
"deblended.chi2dof.dipole.%s %f" % (
550 suffix, peak.psfFitChisq / peak.psfFitDof))
551 self.log.info(
"deblended.flux.dipole.psf.%s %f" % (
552 suffix, peak.psfFitFlux * np.sum(peak.templateMaskedImage.getImage().getArray())))
553 peakList.push_back(peak.peak)
554 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.