lsst.meas.extensions.trailedSources main-geebf31c2e0+7a498dcc28
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
35__all__ = ("SingleFrameNaiveTrailConfig", "SingleFrameNaiveTrailPlugin")
36
37
38class SingleFrameNaiveTrailConfig(SingleFramePluginConfig):
39 """Config class for SingleFrameNaiveTrailPlugin.
40 """
41 pass
42
43
44@register("ext_trailedSources_Naive")
45class SingleFrameNaiveTrailPlugin(SingleFramePlugin):
46 """Naive trailed source measurement plugin
47
48 Measures the length, angle from +x-axis, and end points of an extended
49 source using the second moments.
50
51 Parameters
52 ----------
53 config: `SingleFrameNaiveTrailConfig`
54 Plugin configuration.
55 name: `str`
56 Plugin name.
57 schema: `lsst.afw.table.Schema`
58 Schema for the output catalog.
60 Metadata to be attached to output catalog.
61
62 Notes
63 -----
64 This measurement plugin aims to utilize the already measured adaptive
65 second moments to naively estimate the length and angle, and thus
66 end-points, of a fast-moving, trailed source. The length is solved for via
67 finding the root of the difference between the numerical (stack computed)
68 and the analytic adaptive second moments. The angle, theta, from the x-axis
69 is also computed via adaptive moments: theta = arctan(2*Ixy/(Ixx - Iyy))/2.
70 The end points of the trail are then given by (xc +/- (L/2)*cos(theta),
71 yc +/- (L/2)*sin(theta)), with xc and yc being the centroid coordinates.
72
73 See also
74 --------
75 lsst.meas.base.SingleFramePlugin
76 """
77
78 ConfigClass = SingleFrameNaiveTrailConfig
79
80 @classmethod
82 # Needs centroids, shape, and flux measurements.
83 # VeresPlugin is run after, which requires image data.
84 return cls.APCORR_ORDER + 0.1
85
86 def __init__(self, config, name, schema, metadata):
87 super().__init__(config, name, schema, metadata)
88
89 # Measurement Keys
90 self.keyRakeyRa = schema.addField(name + "_ra", type="D", doc="Trail centroid right ascension.")
91 self.keyDeckeyDec = schema.addField(name + "_dec", type="D", doc="Trail centroid declination.")
92 self.keyX0keyX0 = schema.addField(name + "_x0", type="D", doc="Trail head X coordinate.", units="pixel")
93 self.keyY0keyY0 = schema.addField(name + "_y0", type="D", doc="Trail head Y coordinate.", units="pixel")
94 self.keyX1keyX1 = schema.addField(name + "_x1", type="D", doc="Trail tail X coordinate.", units="pixel")
95 self.keyY1keyY1 = schema.addField(name + "_y1", type="D", doc="Trail tail Y coordinate.", units="pixel")
96 self.keyFluxkeyFlux = schema.addField(name + "_flux", type="D", doc="Trailed source flux.", units="count")
97 self.keyLkeyL = schema.addField(name + "_length", type="D", doc="Trail length.", units="pixel")
98 self.keyAnglekeyAngle = schema.addField(name + "_angle", type="D", doc="Angle measured from +x-axis.")
99
100 # Measurement Error Keys
101 self.keyX0ErrkeyX0Err = schema.addField(name + "_x0Err", type="D",
102 doc="Trail head X coordinate error.", units="pixel")
103 self.keyY0ErrkeyY0Err = schema.addField(name + "_y0Err", type="D",
104 doc="Trail head Y coordinate error.", units="pixel")
105 self.keyX1ErrkeyX1Err = schema.addField(name + "_x1Err", type="D",
106 doc="Trail tail X coordinate error.", units="pixel")
107 self.keyY1ErrkeyY1Err = schema.addField(name + "_y1Err", type="D",
108 doc="Trail tail Y coordinate error.", units="pixel")
109
110 flagDefs = FlagDefinitionList()
111 flagDefs.addFailureFlag("No trailed-source measured")
112 self.NO_FLUXNO_FLUX = flagDefs.add("flag_noFlux", "No suitable prior flux measurement")
113 self.NO_CONVERGENO_CONVERGE = flagDefs.add("flag_noConverge", "The root finder did not converge")
114 self.NO_SIGMANO_SIGMA = flagDefs.add("flag_noSigma", "No PSF width (sigma)")
115 self.flagHandlerflagHandler = FlagHandler.addFields(schema, name, flagDefs)
116
117 self.centriodExtractorcentriodExtractor = SafeCentroidExtractor(schema, name)
118
119 def measure(self, measRecord, exposure):
120 """Run the Naive trailed source measurement algorithm.
121
122 Parameters
123 ----------
124 measRecord : `lsst.afw.table.SourceRecord`
125 Record describing the object being measured.
126 exposure : `lsst.afw.image.Exposure`
127 Pixel data to be measured.
128
129 See also
130 --------
131 lsst.meas.base.SingleFramePlugin.measure
132 """
133
134 # Get the SdssShape centroid or fall back to slot
135 xc = measRecord.get("base_SdssShape_x")
136 yc = measRecord.get("base_SdssShape_y")
137 if not np.isfinite(xc) or not np.isfinite(yc):
138 xc, yc = self.centriodExtractorcentriodExtractor(measRecord, self.flagHandlerflagHandler)
139
140 ra, dec = self.computeRaDeccomputeRaDec(exposure, xc, yc)
141
142 Ixx, Iyy, Ixy = measRecord.getShape().getParameterVector()
143 xmy = Ixx - Iyy
144 xpy = Ixx + Iyy
145 xmy2 = xmy*xmy
146 xy2 = Ixy*Ixy
147 a2 = 0.5 * (xpy + np.sqrt(xmy2 + 4.0*xy2))
148
149 # Get the width of the PSF at the center of the trail
150 center = Point2D(xc, yc)
151 sigma = exposure.getPsf().computeShape(center).getTraceRadius()
152 if not np.isfinite(sigma):
153 raise MeasurementError(self.NO_SIGMANO_SIGMA, self.NO_SIGMANO_SIGMA.number)
154
155 length, results = self.findLengthfindLength(a2, sigma*sigma)
156 if not results.converged:
157 lsst.log.info(results.flag)
158 raise MeasurementError(self.NO_CONVERGENO_CONVERGE.doc, self.NO_CONVERGENO_CONVERGE.number)
159
160 theta = 0.5 * np.arctan2(2.0 * Ixy, xmy)
161 a = length/2.0
162 dydt = a*np.cos(theta)
163 dxdt = a*np.sin(theta)
164 x0 = xc - dydt
165 y0 = yc - dxdt
166 x1 = xc + dydt
167 y1 = yc + dxdt
168
169 # For now, use the shape flux.
170 flux = measRecord.get("base_SdssShape_instFlux")
171
172 # Fall back to aperture flux
173 if not np.isfinite(flux):
174 if np.isfinite(measRecord.getApInstFlux()):
175 flux = measRecord.getApInstFlux()
176 else:
177 raise MeasurementError(self.NO_FLUXNO_FLUX.doc, self.NO_FLUXNO_FLUX.number)
178
179 # Propagate errors from second moments
180 xcErr2, ycErr2 = np.diag(measRecord.getCentroidErr())
181 IxxErr2, IyyErr2, IxyErr2 = np.diag(measRecord.getShapeErr())
182 desc = np.sqrt(xmy2 + 4.0*xy2) # Descriminant^1/2 of EV equation
183 denom = 2*np.sqrt(2.0*(Ixx + np.sqrt(4.0*xy2 + xmy2 + Iyy))) # Denominator for dadIxx and dadIyy
184 dadIxx = (1.0 + (xmy/desc)) / denom
185 dadIyy = (1.0 - (xmy/desc)) / denom
186 dadIxy = (4.0*Ixy) / (desc * denom)
187 aErr2 = IxxErr2*dadIxx*dadIxx + IyyErr2*dadIyy*dadIyy + IxyErr2*dadIxy*dadIxy
188 thetaErr2 = ((IxxErr2 + IyyErr2)*xy2 + xmy2*IxyErr2) / (desc*desc*desc*desc)
189
190 dxda = np.cos(theta)
191 dyda = np.sin(theta)
192 xErr2 = aErr2*dxda*dxda + thetaErr2*dxdt*dxdt
193 yErr2 = aErr2*dyda*dyda + thetaErr2*dydt*dydt
194 x0Err = np.sqrt(xErr2 + xcErr2) # Same for x1
195 y0Err = np.sqrt(yErr2 + ycErr2) # Same for y1
196
197 # Set flags
198 measRecord.set(self.keyRakeyRa, ra)
199 measRecord.set(self.keyDeckeyDec, dec)
200 measRecord.set(self.keyX0keyX0, x0)
201 measRecord.set(self.keyY0keyY0, y0)
202 measRecord.set(self.keyX1keyX1, x1)
203 measRecord.set(self.keyY1keyY1, y1)
204 measRecord.set(self.keyFluxkeyFlux, flux)
205 measRecord.set(self.keyLkeyL, length)
206 measRecord.set(self.keyAnglekeyAngle, theta)
207 measRecord.set(self.keyX0ErrkeyX0Err, x0Err)
208 measRecord.set(self.keyY0ErrkeyY0Err, y0Err)
209 measRecord.set(self.keyX1ErrkeyX1Err, x0Err)
210 measRecord.set(self.keyY1ErrkeyY1Err, y0Err)
211
212 def fail(self, measRecord, error=None):
213 """Record failure
214
215 See also
216 --------
217 lsst.meas.base.SingleFramePlugin.fail
218 """
219 if error is None:
220 self.flagHandlerflagHandler.handleFailure(measRecord)
221 else:
222 self.flagHandlerflagHandler.handleFailure(measRecord, error.cpp)
223
224 def _computeSecondMomentDiff(self, z, c):
225 """Compute difference of the numerical and analytic second moments.
226
227 Parameters
228 ----------
229 z : `float`
230 Proportional to the length of the trail. (see notes)
231 c : `float`
232 Constant (see notes)
233
234 Returns
235 -------
236 diff : `float`
237 Difference in numerical and analytic second moments.
238
239 Notes
240 -----
241 This is a simplified expression for the difference between the stack
242 computed adaptive second-moment and the analytic solution. The variable
243 z is proportional to the length such that L = 2*z*sqrt(2*(Ixx+Iyy)),
244 and c is a constant (c = 4*Ixx/((Ixx+Iyy)*sqrt(pi))). Both have been
245 defined to avoid unnecessary floating-point operations in the root
246 finder.
247 """
248
249 diff = erf(z) - c*z*np.exp(-z*z)
250 return diff
251
252 def findLength(self, Ixx, Iyy):
253 """Find the length of a trail, given adaptive second-moments.
254
255 Uses a root finder to compute the length of a trail corresponding to
256 the adaptive second-moments computed by previous measurements
257 (ie. SdssShape).
258
259 Parameters
260 ----------
261 Ixx : `float`
262 Adaptive second-moment along x-axis.
263 Iyy : `float`
264 Adaptive second-moment along y-axis.
265
266 Returns
267 -------
268 length : `float`
269 Length of the trail.
270 results : `scipy.optimize.RootResults`
271 Contains messages about convergence from the root finder.
272 """
273
274 xpy = Ixx + Iyy
275 c = 4.0*Ixx/(xpy*np.sqrt(np.pi))
276
277 # Given a 'c' in (c_min, c_max], the root is contained in (0,1].
278 # c_min is given by the case: Ixx == Iyy, ie. a point source.
279 # c_max is given by the limit Ixx >> Iyy.
280 # Emperically, 0.001 is a suitable lower bound, assuming Ixx > Iyy.
281 z, results = sciOpt.brentq(lambda z: self._computeSecondMomentDiff_computeSecondMomentDiff(z, c),
282 0.001, 1.0, full_output=True)
283
284 length = 2.0*z*np.sqrt(2.0*xpy)
285 return length, results
286
287 def computeRaDec(self, exposure, x, y):
288 """Convert pixel coordinates to RA and Dec.
289
290 Parameters
291 ----------
292 exposure : `lsst.afw.image.ExposureF`
293 Exposure object containing the WCS.
294 x : `float`
295 x coordinate of the trail centroid
296 y : `float`
297 y coodinate of the trail centroid
298
299 Returns
300 -------
301 ra : `float`
302 Right ascension.
303 dec : `float`
304 Declination.
305 """
306
307 wcs = exposure.getWcs()
308 center = wcs.pixelToSky(Point2D(x, y))
309 ra = center.getRa().asDegrees()
310 dec = center.getDec().asDegrees()
311 return ra, dec