29from lsst.meas.base import SingleFrameMeasurementTask, SingleFrameMeasurementConfig, \
30 SingleFramePluginConfig, SingleFramePlugin
32from lsst.utils.logging
import getLogger
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
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.")
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
134 ConfigClass = DipoleMeasurementConfig
135 _DefaultName = "dipoleMeasurement"
143 """Functor class to check whether a diaSource has flags set that should cause it to be labeled bad."""
146 self.
badFlags = [
'base_PixelFlags_flag_edge',
'base_PixelFlags_flag_interpolatedCenter',
147 'base_PixelFlags_flag_saturatedCenter']
148 if badFlags
is not None:
149 for flag
in badFlags:
151 self.
keys = [sources.getSchema().find(name).key
for name
in self.
badFlags]
152 self.
keys.append(sources.table.getCentroidFlagKey())
155 """Call the source flag checker on a single Source
160 Source that will be examined
169 """Functor class that provides (S/N, position, orientation) of measured dipoles"""
175 """Parse information returned from dipole measurement
180 The source that will be examined"""
181 return self.getSn(source), self.getCentroid(source), self.getOrientation(source)
184 """Get the total signal-to-noise of the dipole; total S/N is from positive and negative lobe
189 The source that will be examined"""
191 posflux = source.get("ip_diffim_PsfDipoleFlux_pos_instFlux")
192 posfluxErr = source.get(
"ip_diffim_PsfDipoleFlux_pos_instFluxErr")
193 negflux = source.get(
"ip_diffim_PsfDipoleFlux_neg_instFlux")
194 negfluxErr = source.get(
"ip_diffim_PsfDipoleFlux_neg_instFluxErr")
197 if (posflux < 0)
is (negflux < 0):
200 return np.sqrt((posflux/posfluxErr)**2 + (negflux/negfluxErr)**2)
203 """Get the centroid of the dipole; average of positive and negative lobe
208 The source that will be examined"""
210 negCenX = source.get("ip_diffim_PsfDipoleFlux_neg_centroid_x")
211 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
212 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
213 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
214 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
218 0.5*(negCenY+posCenY))
222 """Calculate the orientation of dipole; vector from negative to positive lobe
227 The source that will be examined"""
229 negCenX = source.get("ip_diffim_PsfDipoleFlux_neg_centroid_x")
230 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
231 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
232 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
233 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
236 dx, dy = posCenX-negCenX, posCenY-negCenY
237 angle =
geom.Angle(np.arctan2(dx, dy), geom.radians)
241 """Display debugging information on the detected dipoles
246 Image the dipoles were measured on
248 The set of diaSources that were measured"""
254 if not maskTransparency:
255 maskTransparency = 90
256 disp = afwDisplay.Display(frame=lsstDebug.frame)
257 disp.setMaskTransparency(maskTransparency)
260 if display
and displayDiaSources:
261 with disp.Buffering():
262 for source
in sources:
263 cenX, cenY = source.get(
"ipdiffim_DipolePsfFlux_centroid")
264 if np.isinf(cenX)
or np.isinf(cenY):
265 cenX, cenY = source.getCentroid()
267 isdipole = source.get(
"ip_diffim_ClassificationDipole_value")
268 if isdipole
and np.isfinite(isdipole):
270 ctype = afwDisplay.GREEN
273 ctype = afwDisplay.RED
275 disp.dot(
"o", cenX, cenY, size=2, ctype=ctype)
277 negCenX = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_x")
278 negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
279 posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
280 posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
281 if (np.isinf(negCenX)
or np.isinf(negCenY)
or np.isinf(posCenX)
or np.isinf(posCenY)):
284 disp.line([(negCenX, negCenY), (posCenX, posCenY)], ctype=afwDisplay.YELLOW)
290 """Functor to deblend a source as a dipole, and return a new source with deblended footprints.
292 This necessarily overrides some of the functionality from
293 meas_algorithms/python/lsst/meas/algorithms/deblend.py since we
294 need a single source that contains the blended peaks,
not
295 multiple children sources. This directly calls the core
296 deblending code deblendBaseline.deblend (optionally _fitPsf
for
299 Not actively being used, but there
is a unit test
for it
in
308 self.
log = getLogger(
'lsst.ip.diffim.DipoleDeblender')
312 fp = source.getFootprint()
313 peaks = fp.getPeaks()
314 peaksF = [pk.getF()
for pk
in peaks]
317 fmask.setXY0(fbb.getMinX(), fbb.getMinY())
318 fp.spans.setMask(fmask, 1)
320 psf = exposure.getPsf()
321 psfSigPix = psf.computeShape(psf.getAveragePosition()).getDeterminantRadius()
323 subimage = afwImage.ExposureF(exposure, bbox=fbb, deep=
True)
324 cpsf = deblendBaseline.CachingPsf(psf)
328 return source.getTable().copyRecord(source)
331 speaks = [(p.getPeakValue(), p)
for p
in peaks]
333 dpeaks = [speaks[0][1], speaks[-1][1]]
342 fpres = deblendBaseline.deblend(fp, exposure.getMaskedImage(), psf, psfFwhmPix,
351 fpres = deblendBaseline.DeblenderResult(fp, exposure.getMaskedImage(), psf, psfFwhmPix, self.
log)
353 for pki, (pk, pkres, pkF)
in enumerate(zip(dpeaks, fpres.deblendedParents[0].peaks, peaksF)):
354 self.
log.debug(
'Peak %i', pki)
355 deblendBaseline._fitPsf(fp, fmask, pk, pkF, pkres, fbb, dpeaks, peaksF, self.
log,
361 deblendedSource = source.getTable().copyRecord(source)
362 deblendedSource.setParent(source.getId())
363 peakList = deblendedSource.getFootprint().getPeaks()
366 for i, peak
in enumerate(fpres.deblendedParents[0].peaks):
367 if peak.psfFitFlux > 0:
371 c = peak.psfFitCenter
372 self.
log.info(
"deblended.centroid.dipole.psf.%s %f %f",
374 self.
log.info(
"deblended.chi2dof.dipole.%s %f",
375 suffix, peak.psfFitChisq / peak.psfFitDof)
376 self.
log.info(
"deblended.flux.dipole.psf.%s %f",
377 suffix, peak.psfFitFlux * np.sum(peak.templateImage.array))
378 peakList.append(peak.peak)
379 return deblendedSource
table::Key< std::string > object
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Represent a 2-dimensional array of bitmask pixels.
Record class that contains measurements made on a single exposure.
A class representing an angle.
__init__(self, config, name, schema, metadata)
fail(self, measRecord, error=None)
measure(self, measRecord, exposure)
getOrientation(self, source)
getCentroid(self, source)
displayDipoles(self, exposure, sources)
__call__(self, source, exposure)
__init__(self, sources, badFlags=None)
fail(self, measRecord, error=None)