lsst.meas.extensions.trailedSources gbacec6188e+b63bd7fcda
NaivePlugin.py
Go to the documentation of this file.
2# This file is part of meas_extensions_trailedSources.
3#
4# Developed for the LSST Data Management System.
5# This product includes software developed by the LSST Project
6# (http://www.lsst.org).
7# See the COPYRIGHT file at the top-level directory of this distribution
8# for details of code ownership.
9#
10# This program is free software: you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation, either version 3 of the License, or
13# (at your option) any later version.
14#
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with this program. If not, see <http://www.gnu.org/licenses/>.
22#
23
24import numpy as np
25import scipy.optimize as sciOpt
26from scipy.special import erf
27
28import lsst.log
29from lsst.geom import Point2D
30from lsst.meas.base.pluginRegistry import register
31from lsst.meas.base import SingleFramePlugin, SingleFramePluginConfig
32from lsst.meas.base import FlagHandler, FlagDefinitionList, SafeCentroidExtractor
33from lsst.meas.base import MeasurementError
34
35from ._trailedSources import VeresModel
36from .utils import getMeasurementCutout
37
38__all__ = ("SingleFrameNaiveTrailConfig", "SingleFrameNaiveTrailPlugin")
39
40
41class SingleFrameNaiveTrailConfig(SingleFramePluginConfig):
42 """Config class for SingleFrameNaiveTrailPlugin.
43 """
44 pass
45
46
47@register("ext_trailedSources_Naive")
48class SingleFrameNaiveTrailPlugin(SingleFramePlugin):
49 """Naive trailed source measurement plugin
50
51 Measures the length, angle from +x-axis, and end points of an extended
52 source using the second moments.
53
54 Parameters
55 ----------
56 config: `SingleFrameNaiveTrailConfig`
57 Plugin configuration.
58 name: `str`
59 Plugin name.
60 schema: `lsst.afw.table.Schema`
61 Schema for the output catalog.
63 Metadata to be attached to output catalog.
64
65 Notes
66 -----
67 This measurement plugin aims to utilize the already measured adaptive
68 second moments to naively estimate the length and angle, and thus
69 end-points, of a fast-moving, trailed source. The length is solved for via
70 finding the root of the difference between the numerical (stack computed)
71 and the analytic adaptive second moments. The angle, theta, from the x-axis
72 is also computed via adaptive moments: theta = arctan(2*Ixy/(Ixx - Iyy))/2.
73 The end points of the trail are then given by (xc +/- (length/2)*cos(theta)
74 and yc +/- (length/2)*sin(theta)), with xc and yc being the centroid
75 coordinates.
76
77 See also
78 --------
79 lsst.meas.base.SingleFramePlugin
80 """
81
82 ConfigClass = SingleFrameNaiveTrailConfig
83
84 @classmethod
86 # Needs centroids, shape, and flux measurements.
87 # VeresPlugin is run after, which requires image data.
88 return cls.APCORR_ORDER + 0.1
89
90 def __init__(self, config, name, schema, metadata):
91 super().__init__(config, name, schema, metadata)
92
93 # Measurement Keys
94 self.keyRakeyRa = schema.addField(name + "_ra", type="D", doc="Trail centroid right ascension.")
95 self.keyDeckeyDec = schema.addField(name + "_dec", type="D", doc="Trail centroid declination.")
96 self.keyX0keyX0 = schema.addField(name + "_x0", type="D", doc="Trail head X coordinate.", units="pixel")
97 self.keyY0keyY0 = schema.addField(name + "_y0", type="D", doc="Trail head Y coordinate.", units="pixel")
98 self.keyX1keyX1 = schema.addField(name + "_x1", type="D", doc="Trail tail X coordinate.", units="pixel")
99 self.keyY1keyY1 = schema.addField(name + "_y1", type="D", doc="Trail tail Y coordinate.", units="pixel")
100 self.keyFluxkeyFlux = schema.addField(name + "_flux", type="D", doc="Trailed source flux.", units="count")
101 self.keyLengthkeyLength = schema.addField(name + "_length", type="D", doc="Trail length.", units="pixel")
102 self.keyAnglekeyAngle = schema.addField(name + "_angle", type="D", doc="Angle measured from +x-axis.")
103
104 # Measurement Error Keys
105 self.keyX0ErrkeyX0Err = schema.addField(name + "_x0Err", type="D",
106 doc="Trail head X coordinate error.", units="pixel")
107 self.keyY0ErrkeyY0Err = schema.addField(name + "_y0Err", type="D",
108 doc="Trail head Y coordinate error.", units="pixel")
109 self.keyX1ErrkeyX1Err = schema.addField(name + "_x1Err", type="D",
110 doc="Trail tail X coordinate error.", units="pixel")
111 self.keyY1ErrkeyY1Err = schema.addField(name + "_y1Err", type="D",
112 doc="Trail tail Y coordinate error.", units="pixel")
113
114 flagDefs = FlagDefinitionList()
115 flagDefs.addFailureFlag("No trailed-source measured")
116 self.NO_FLUXNO_FLUX = flagDefs.add("flag_noFlux", "No suitable prior flux measurement")
117 self.NO_CONVERGENO_CONVERGE = flagDefs.add("flag_noConverge", "The root finder did not converge")
118 self.NO_SIGMANO_SIGMA = flagDefs.add("flag_noSigma", "No PSF width (sigma)")
119 self.flagHandlerflagHandler = FlagHandler.addFields(schema, name, flagDefs)
120
121 self.centriodExtractorcentriodExtractor = SafeCentroidExtractor(schema, name)
122
123 def measure(self, measRecord, exposure):
124 """Run the Naive trailed source measurement algorithm.
125
126 Parameters
127 ----------
128 measRecord : `lsst.afw.table.SourceRecord`
129 Record describing the object being measured.
130 exposure : `lsst.afw.image.Exposure`
131 Pixel data to be measured.
132
133 See also
134 --------
135 lsst.meas.base.SingleFramePlugin.measure
136 """
137
138 # Get the SdssShape centroid or fall back to slot
139 xc = measRecord.get("base_SdssShape_x")
140 yc = measRecord.get("base_SdssShape_y")
141 if not np.isfinite(xc) or not np.isfinite(yc):
142 xc, yc = self.centriodExtractorcentriodExtractor(measRecord, self.flagHandlerflagHandler)
143
144 ra, dec = self.computeRaDeccomputeRaDec(exposure, xc, yc)
145
146 Ixx, Iyy, Ixy = measRecord.getShape().getParameterVector()
147 xmy = Ixx - Iyy
148 xpy = Ixx + Iyy
149 xmy2 = xmy*xmy
150 xy2 = Ixy*Ixy
151 a2 = 0.5 * (xpy + np.sqrt(xmy2 + 4.0*xy2))
152
153 # Get the width of the PSF at the center of the trail
154 center = Point2D(xc, yc)
155 sigma = exposure.getPsf().computeShape(center).getTraceRadius()
156 if not np.isfinite(sigma):
157 raise MeasurementError(self.NO_SIGMANO_SIGMA, self.NO_SIGMANO_SIGMA.number)
158
159 # Check if moments are wieghted
160 if measRecord.get("base_SdssShape_flag_unweighted"):
161 lsst.log.debug("Unweighed")
162 length = np.sqrt(6.0*(a2 - 2*sigma*sigma))
163 else:
164 lsst.log.debug("Weighted")
165 length, results = self.findLengthfindLength(a2, sigma*sigma)
166 if not results.converged:
167 lsst.log.info(results.flag)
168 raise MeasurementError(self.NO_CONVERGENO_CONVERGE.doc, self.NO_CONVERGENO_CONVERGE.number)
169
170 theta = 0.5 * np.arctan2(2.0 * Ixy, xmy)
171 a = length/2.0
172 dydt = a*np.cos(theta)
173 dxdt = a*np.sin(theta)
174 x0 = xc - dydt
175 y0 = yc - dxdt
176 x1 = xc + dydt
177 y1 = yc + dxdt
178
179 # Get a cutout of the object from the exposure
180 # cutout = getMeasurementCutout(exposure, xc, yc, L, sigma)
181 cutout = getMeasurementCutout(measRecord, exposure)
182
183 # Compute flux assuming fixed parameters for VeresModel
184 params = np.array([xc, yc, 1.0, length, theta]) # Flux = 1.0
185 model = VeresModel(cutout)
186 modelArray = model.computeModelImage(params).array.flatten()
187 dataArray = cutout.image.array.flatten()
188 flux = np.dot(dataArray, modelArray) / np.dot(modelArray, modelArray)
189
190 # Fall back to aperture flux
191 if not np.isfinite(flux):
192 if np.isfinite(measRecord.getApInstFlux()):
193 flux = measRecord.getApInstFlux()
194 else:
195 raise MeasurementError(self.NO_FLUXNO_FLUX.doc, self.NO_FLUXNO_FLUX.number)
196
197 # Propagate errors from second moments
198 xcErr2, ycErr2 = np.diag(measRecord.getCentroidErr())
199 IxxErr2, IyyErr2, IxyErr2 = np.diag(measRecord.getShapeErr())
200 desc = np.sqrt(xmy2 + 4.0*xy2) # Descriminant^1/2 of EV equation
201 denom = 2*np.sqrt(2.0*(Ixx + np.sqrt(4.0*xy2 + xmy2 + Iyy))) # Denominator for dadIxx and dadIyy
202 dadIxx = (1.0 + (xmy/desc)) / denom
203 dadIyy = (1.0 - (xmy/desc)) / denom
204 dadIxy = (4.0*Ixy) / (desc * denom)
205 aErr2 = IxxErr2*dadIxx*dadIxx + IyyErr2*dadIyy*dadIyy + IxyErr2*dadIxy*dadIxy
206 thetaErr2 = ((IxxErr2 + IyyErr2)*xy2 + xmy2*IxyErr2) / (desc*desc*desc*desc)
207
208 dxda = np.cos(theta)
209 dyda = np.sin(theta)
210 xErr2 = aErr2*dxda*dxda + thetaErr2*dxdt*dxdt
211 yErr2 = aErr2*dyda*dyda + thetaErr2*dydt*dydt
212 x0Err = np.sqrt(xErr2 + xcErr2) # Same for x1
213 y0Err = np.sqrt(yErr2 + ycErr2) # Same for y1
214
215 # Set flags
216 measRecord.set(self.keyRakeyRa, ra)
217 measRecord.set(self.keyDeckeyDec, dec)
218 measRecord.set(self.keyX0keyX0, x0)
219 measRecord.set(self.keyY0keyY0, y0)
220 measRecord.set(self.keyX1keyX1, x1)
221 measRecord.set(self.keyY1keyY1, y1)
222 measRecord.set(self.keyFluxkeyFlux, flux)
223 measRecord.set(self.keyLengthkeyLength, length)
224 measRecord.set(self.keyAnglekeyAngle, theta)
225 measRecord.set(self.keyX0ErrkeyX0Err, x0Err)
226 measRecord.set(self.keyY0ErrkeyY0Err, y0Err)
227 measRecord.set(self.keyX1ErrkeyX1Err, x0Err)
228 measRecord.set(self.keyY1ErrkeyY1Err, y0Err)
229
230 def fail(self, measRecord, error=None):
231 """Record failure
232
233 See also
234 --------
235 lsst.meas.base.SingleFramePlugin.fail
236 """
237 if error is None:
238 self.flagHandlerflagHandler.handleFailure(measRecord)
239 else:
240 self.flagHandlerflagHandler.handleFailure(measRecord, error.cpp)
241
242 def _computeSecondMomentDiff(self, z, c):
243 """Compute difference of the numerical and analytic second moments.
244
245 Parameters
246 ----------
247 z : `float`
248 Proportional to the length of the trail. (see notes)
249 c : `float`
250 Constant (see notes)
251
252 Returns
253 -------
254 diff : `float`
255 Difference in numerical and analytic second moments.
256
257 Notes
258 -----
259 This is a simplified expression for the difference between the stack
260 computed adaptive second-moment and the analytic solution. The variable
261 z is proportional to the length such that length=2*z*sqrt(2*(Ixx+Iyy)),
262 and c is a constant (c = 4*Ixx/((Ixx+Iyy)*sqrt(pi))). Both have been
263 defined to avoid unnecessary floating-point operations in the root
264 finder.
265 """
266
267 diff = erf(z) - c*z*np.exp(-z*z)
268 return diff
269
270 def findLength(self, Ixx, Iyy):
271 """Find the length of a trail, given adaptive second-moments.
272
273 Uses a root finder to compute the length of a trail corresponding to
274 the adaptive second-moments computed by previous measurements
275 (ie. SdssShape).
276
277 Parameters
278 ----------
279 Ixx : `float`
280 Adaptive second-moment along x-axis.
281 Iyy : `float`
282 Adaptive second-moment along y-axis.
283
284 Returns
285 -------
286 length : `float`
287 Length of the trail.
288 results : `scipy.optimize.RootResults`
289 Contains messages about convergence from the root finder.
290 """
291
292 xpy = Ixx + Iyy
293 c = 4.0*Ixx/(xpy*np.sqrt(np.pi))
294
295 # Given a 'c' in (c_min, c_max], the root is contained in (0,1].
296 # c_min is given by the case: Ixx == Iyy, ie. a point source.
297 # c_max is given by the limit Ixx >> Iyy.
298 # Emperically, 0.001 is a suitable lower bound, assuming Ixx > Iyy.
299 z, results = sciOpt.brentq(lambda z: self._computeSecondMomentDiff_computeSecondMomentDiff(z, c),
300 0.001, 1.0, full_output=True)
301
302 length = 2.0*z*np.sqrt(2.0*xpy)
303 return length, results
304
305 def computeRaDec(self, exposure, x, y):
306 """Convert pixel coordinates to RA and Dec.
307
308 Parameters
309 ----------
310 exposure : `lsst.afw.image.ExposureF`
311 Exposure object containing the WCS.
312 x : `float`
313 x coordinate of the trail centroid
314 y : `float`
315 y coodinate of the trail centroid
316
317 Returns
318 -------
319 ra : `float`
320 Right ascension.
321 dec : `float`
322 Declination.
323 """
324
325 wcs = exposure.getWcs()
326 center = wcs.pixelToSky(Point2D(x, y))
327 ra = center.getRa().asDegrees()
328 dec = center.getDec().asDegrees()
329 return ra, dec
def getMeasurementCutout(measRecord, exposure)
Definition: utils.py:28