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"
55 SingleFrameMeasurementConfig.setDefaults(self)
56 self.
plugins = [
"ip_diffim_NaiveDipoleCentroid",
57 "ip_diffim_NaiveDipoleFlux",
58 "ip_diffim_PsfDipoleFlux"]
59 self.slots.psfFlux =
None
60 self.slots.calibFlux =
None
61 self.slots.modelFlux =
None
62 self.slots.apFlux =
None
63 self.slots.instFlux =
None
64 self.slots.shape =
None
65 self.slots.centroid =
"ip_diffim_NaiveDipoleCentroid_pos"
76 \anchor DipoleMeasurementTask_
78 \brief Measurement of Sources, specifically ones from difference images, for characterization as dipoles
80 \section ip_diffim_dipolemeas_Contents Contents
82 - \ref ip_diffim_dipolemeas_Purpose
83 - \ref ip_diffim_dipolemeas_Initialize
84 - \ref ip_diffim_dipolemeas_IO
85 - \ref ip_diffim_dipolemeas_Config
86 - \ref ip_diffim_dipolemeas_Metadata
87 - \ref ip_diffim_dipolemeas_Debug
88 - \ref ip_diffim_dipolemeas_Example
90 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
92 \section ip_diffim_dipolemeas_Purpose Description
94 This class provies a specialized set of Source measurements that allow the user to test the hypothesis that
95 the Source is a dipole. This includes a set of measurements derived from intermediate base classes
96 DipoleCentroidAlgorithm and DipoleFluxAlgorithm. Their respective algorithm control classes are defined in
97 DipoleCentroidControl and DipoleFluxControl. Each centroid and flux measurement will have .neg (negative)
98 and .pos (positive lobe) fields.
100 The first set of measurements uses a "naive" alrogithm for centroid and flux measurements, implemented in
101 NaiveDipoleCentroidControl and NaiveDipoleFluxControl. The algorithm uses a naive 3x3 weighted moment around
102 the nominal centroids of each peak in the Source Footprint. These algorithms fill the table fields
103 centroid.dipole.naive.* and flux.dipole.naive.*.
105 The second set of measurements undertakes a joint-Psf model on the negative and positive lobe simultaneously.
106 This fit simultaneously solves for the negative and positive lobe centroids and fluxes using non-linear
107 least squares minimization. The fields are stored in table elements flux.dipole.psf.*.
109 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
111 \section ip_diffim_dipolemeas_Initialize Task initialization
113 \copydoc \_\_init\_\_
115 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
117 \section ip_diffim_dipolemeas_IO Invoking the Task
121 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
123 \section ip_diffim_dipolemeas_Config Configuration parameters
125 See \ref DipoleMeasurementConfig and \ref DipoleClassificationConfig
127 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
129 \section ip_diffim_dipolemeas_Metadata Quantities set in Metadata
131 No specific values are set in the Task metadata. However, the Source schema are modified to store the
132 results of the dipole-specific measurements. These additional fields include:
134 . classification.dipole : probability of being a dipole
136 . centroid.dipole.naive.pos : unweighted 3x3 first moment centroid
138 . centroid.dipole.naive.pos.err : covariance matrix for centroid.dipole.naive.pos
140 . centroid.dipole.naive.pos.flags : set if the centroid.dipole.naive.pos measurement did not fully succeed
142 . centroid.dipole.naive.neg : unweighted 3x3 first moment centroid
144 . centroid.dipole.naive.neg.err : covariance matrix for centroid.dipole.naive.neg
146 . centroid.dipole.naive.neg.flags : set if the centroid.dipole.naive.neg measurement did not fully succeed
148 . flux.dipole.naive.pos : raw flux counts
150 . flux.dipole.naive.pos.err : uncertainty for flux.dipole.naive.pos
152 . flux.dipole.naive.pos.flags : set if the flux.dipole.naive.pos measurement failed
154 . flux.dipole.naive.neg : raw flux counts
156 . flux.dipole.naive.neg.err : uncertainty for flux.dipole.naive.neg
158 . flux.dipole.naive.neg.flags : set if the flux.dipole.naive.neg measurement failed
160 . flux.dipole.naive.npos : number of positive pixels
162 . flux.dipole.naive.nneg : number of negative pixels
164 . flux.dipole.psf.pos : jointly fitted psf flux counts
166 . flux.dipole.psf.pos.err : uncertainty for flux.dipole.psf.pos
168 . flux.dipole.psf.pos.flags : set if the flux.dipole.psf.pos measurement failed
170 . flux.dipole.psf.neg : jointly fitted psf flux counts
172 . flux.dipole.psf.neg.err : uncertainty for flux.dipole.psf.neg
174 . flux.dipole.psf.neg.flags : set if the flux.dipole.psf.neg measurement failed
176 . flux.dipole.psf.chi2dof : chi2 per degree of freedom of fit
178 . flux.dipole.psf.centroid : average of the postive and negative lobe positions
180 . flux.dipole.psf.centroid.err : covariance matrix for flux.dipole.psf.centroid
182 . flux.dipole.psf.centroid.flags : set if the flux.dipole.psf.centroid measurement did not fully succeed
184 . flux.dipole.psf.neg.centroid : psf fitted center of negative lobe
186 . flux.dipole.psf.neg.centroid.err : covariance matrix for flux.dipole.psf.neg.centroid
188 . flux.dipole.psf.neg.centroid.flags : set if the flux.dipole.psf.neg.centroid measurement did not fully succeed
190 . flux.dipole.psf.pos.centroid : psf fitted center of positive lobe
192 . flux.dipole.psf.pos.centroid.err : covariance matrix for flux.dipole.psf.pos.centroid
194 . flux.dipole.psf.pos.centroid.flags : set if the flux.dipole.psf.pos.centroid measurement did not fully succeed
196 . flux.dipole.psf.flags.maxpix : set if too large a footprint was sent to the algorithm
198 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
200 \section ip_diffim_dipolemeas_Debug Debug variables
202 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
203 flag \c -d/--debug to import \b debug.py from your \c PYTHONPATH. The relevant contents of debug.py
204 for this Task include:
210 di = lsstDebug.getInfo(name)
211 if name == "lsst.ip.diffim.dipoleMeasurement":
212 di.display = True # enable debug output
213 di.maskTransparency = 90 # ds9 mask transparency
214 di.displayDiaSources = True # show exposure with dipole results
216 lsstDebug.Info = DebugInfo
220 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
222 \section ip_diffim_dipolemeas_Example A complete example of using DipoleMeasurementTask
224 This code is dipoleMeasTask.py in the examples directory, and can be run as \em e.g.
226 examples/dipoleMeasTask.py
227 examples/dipoleMeasTask.py --debug
228 examples/dipoleMeasTask.py --debug --image /path/to/image.fits
231 \dontinclude dipoleMeasTask.py
232 Start the processing by parsing the command line, where the user has the option of enabling debugging output
233 and/or sending their own image for demonstration (in case they have not downloaded the afwdata package).
237 \dontinclude dipoleMeasTask.py
238 The processing occurs in the run function. We first extract an exposure from disk or afwdata, displaying
243 Create a default source schema that we will append fields to as we add more algorithms:
244 \skip makeMinimalSchema
245 \until makeMinimalSchema
247 Create the detection and measurement Tasks, with some minor tweaking of their configs:
251 Having fully initialied the schema, we create a Source table from it:
259 Because we are looking for dipoles, we need to merge the positive and negative detections:
263 Finally, perform measurement (both standard and dipole-specialized) on the merged sources:
267 Optionally display debugging information:
269 \until displayDipoles
270 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
273 ConfigClass = DipoleMeasurementConfig
274 _DefaultName =
"dipoleMeasurement"
275 _ClassificationFlag =
"classification_dipole"
277 def __init__(self, schema, algMetadata=None, **kwds):
278 """!Create the Task, and add Task-specific fields to the provided measurement table schema.
280 @param[in,out] schema Schema object for measurement fields; modified in-place.
281 @param[in,out] algMetadata Passed to MeasureSources object to be filled with
282 metadata by algorithms (e.g. radii for aperture photometry).
283 @param **kwds Passed to Task.__init__.
285 SingleFrameMeasurementTask.__init__(self, schema, algMetadata, **kwds)
290 """!Create the records needed for dipole classification, and run classifier
292 @param[in,out] sources The diaSources to run classification on; modified in in-place
295 self.log.log(self.log.INFO,
"Classifying %d sources" % len(sources))
299 ctrl = self.config.classification
306 for source
in sources:
307 passesSn = self.dipoleAnalysis.getSn(source) > ctrl.minSn
309 negFlux = np.abs(source.get(
"ip_diffim_PsfDipoleFlux_neg_flux"))
310 posFlux = np.abs(source.get(
"ip_diffim_PsfDipoleFlux_pos_flux"))
311 totalFlux = negFlux + posFlux
312 passesFluxNeg = (negFlux / totalFlux) < ctrl.maxFluxRatio
313 passesFluxPos = (posFlux / totalFlux) < ctrl.maxFluxRatio
314 if (passesSn
and passesFluxPos
and passesFluxNeg):
321 def run(self, sources, exposure, **kwds):
322 """!Run dipole measurement and classification
323 @param sources diaSources that will be measured using dipole measurement
324 @param exposure Exposure on which the diaSources were detected
325 @param **kwds Sent to SingleFrameMeasurementTask
328 SingleFrameMeasurementTask.run(self, sources, exposure, **kwds)
336 """!Functor class to check whether a diaSource has flags set that should cause it to be labeled bad."""
340 @param sources Sources that will be measured
341 @param badFlags A list of flags that will be used to determine if there was a measurement problem
343 The list of badFlags will be used to make a list of keys to check for measurement flags on. By
344 default the centroid keys are added to this list"""
346 self.
badFlags = [
'base_PixelFlags_flag_edge',
'base_PixelFlags_flag_interpolatedCenter',
'base_PixelFlags_flag_saturatedCenter']
347 if badFlags
is not None:
348 for flag
in badFlags:
349 self.badFlags.append(flag)
350 self.
keys = [sources.getSchema().find(name).key
for name
in self.
badFlags]
351 self.keys.append(sources.table.getCentroidFlagKey())
354 """!Call the source flag checker on a single Source
356 @param source Source that will be examined"""
363 """!Functor class that provides (S/N, position, orientation) of measured dipoles"""
369 """!Parse information returned from dipole measurement
371 @param source The source that will be examined"""
372 return self.getSn(source), self.getCentroid(source), self.getOrientation(source)
375 """!Get the total signal-to-noise of the dipole; total S/N is from positive and negative lobe
377 @param source The source that will be examined"""
379 posflux = source.get(
"ip_diffim_PsfDipoleFlux_pos_flux")
380 posfluxErr = source.get(
"ip_diffim_PsfDipoleFlux_pos_fluxSigma")
381 negflux = source.get(
"ip_diffim_PsfDipoleFlux_neg_flux")
382 negfluxErr = source.get(
"ip_diffim_PsfDipoleFlux_neg_fluxSigma")
385 if (posflux < 0)
is (negflux < 0):
388 return np.sqrt((posflux/posfluxErr)**2 + (negflux/negfluxErr)**2)
391 """!Get the centroid of the dipole; average of positive and negative lobe
393 @param source The source that will be examined"""
395 negCenX = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_x")
396 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
397 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
398 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
399 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
403 0.5*(negCenY+posCenY))
407 """!Calculate the orientation of dipole; vector from negative to positive lobe
409 @param source The source that will be examined"""
411 negCenX = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_x")
412 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
413 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
414 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
415 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
418 dx, dy = posCenX-negCenX, posCenY-negCenY
423 """!Display debugging information on the detected dipoles
425 @param exposure Image the dipoles were measured on
426 @param sources The set of diaSources that were measured"""
432 if not maskTransparency:
433 maskTransparency = 90
434 ds9.setMaskTransparency(maskTransparency)
435 ds9.mtv(exposure, frame=lsstDebug.frame)
437 if display
and displayDiaSources:
438 with ds9.Buffering():
439 for source
in sources:
440 cenX, cenY = source.get(
"ipdiffim_DipolePsfFlux_centroid")
441 if np.isinf(cenX)
or np.isinf(cenY):
442 cenX, cenY = source.getCentroid()
444 isdipole = source.get(
"classification.dipole")
445 if isdipole
and np.isfinite(isdipole):
452 ds9.dot(
"o", cenX, cenY, size=2, ctype=ctype, frame=lsstDebug.frame)
454 negCenX = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_x")
455 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
456 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
457 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
458 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
461 ds9.line([(negCenX, negCenY), (posCenX, posCenY)], ctype=
"yellow", frame=lsstDebug.frame)
468 """!Functor to deblend a source as a dipole, and return a new source with deblended footprints.
470 This necessarily overrides some of the functionality from
471 meas_algorithms/python/lsst/meas/algorithms/deblend.py since we
472 need a single source that contains the blended peaks, not
473 multiple children sources. This directly calls the core
474 deblending code deblendBaseline.deblend (optionally _fitPsf for
477 Not actively being used, but there is a unit test for it in
486 'lsst.ip.diffim.DipoleDeblender', pexLog.Log.INFO)
490 fp = source.getFootprint()
491 peaks = fp.getPeaks()
492 peaksF = [pk.getF()
for pk
in peaks]
494 fmask = afwImage.MaskU(fbb)
495 fmask.setXY0(fbb.getMinX(), fbb.getMinY())
498 psf = exposure.getPsf()
499 psfSigPix = psf.computeShape().getDeterminantRadius()
501 subimage = afwImage.ExposureF(exposure, fbb,
True)
502 cpsf = deblendBaseline.CachingPsf(psf)
506 return source.getTable().copyRecord(source)
509 speaks = [(p.getPeakValue(), p)
for p
in peaks]
511 dpeaks = [speaks[0][1], speaks[-1][1]]
520 fpres = deblendBaseline.deblend(fp, exposure.getMaskedImage(), psf, psfFwhmPix,
529 fpres = deblendBaseline.PerFootprint()
531 for pki,pk
in enumerate(dpeaks):
532 pkres = deblendBaseline.PerPeak()
535 fpres.peaks.append(pkres)
537 for pki,(pk,pkres,pkF)
in enumerate(zip(dpeaks, fpres.peaks, peaksF)):
538 self.log.logdebug(
'Peak %i' % pki)
539 deblendBaseline._fitPsf(fp, fmask, pk, pkF, pkres, fbb, dpeaks, peaksF, self.
log,
541 subimage.getMaskedImage().getImage(),
542 subimage.getMaskedImage().getVariance(),
546 deblendedSource = source.getTable().copyRecord(source)
547 deblendedSource.setParent(source.getId())
548 peakList = deblendedSource.getFootprint().getPeaks()
551 for i, peak
in enumerate(fpres.peaks):
552 if peak.psfFitFlux > 0:
556 c = peak.psfFitCenter
557 self.log.info(
"deblended.centroid.dipole.psf.%s %f %f" % (
559 self.log.info(
"deblended.chi2dof.dipole.%s %f" % (
560 suffix, peak.psfFitChisq / peak.psfFitDof))
561 self.log.info(
"deblended.flux.dipole.psf.%s %f" % (
562 suffix, peak.psfFitFlux * np.sum(peak.templateImage.getArray())))
563 peakList.append(peak.peak)
564 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.