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