25 import scipy.optimize
as sciOpt
26 from scipy.special
import erf
30 from lsst.meas.base import SingleFramePlugin, SingleFramePluginConfig
31 from lsst.meas.base import FlagHandler, FlagDefinitionList, SafeCentroidExtractor
34 __all__ = (
"SingleFrameNaiveTrailConfig",
"SingleFrameNaiveTrailPlugin")
38 """Config class for SingleFrameNaiveTrailPlugin.
43 @
register(
"ext_trailedSources_Naive")
45 """Naive trailed source measurement plugin
47 Measures the length, angle from +x-axis, and end points of an extended
48 source using the second moments.
52 config: `SingleFrameNaiveTrailConfig`
56 schema: `lsst.afw.table.Schema`
57 Schema for the output catalog.
58 metadata: `lsst.daf.base.PropertySet`
59 Metadata to be attached to output catalog.
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.
74 lsst.meas.base.SingleFramePlugin
77 ConfigClass = SingleFrameNaiveTrailConfig
85 def __init__(self, config, name, schema, metadata):
86 super().
__init__(config, name, schema, metadata)
89 self.
keyX0keyX0 = schema.addField(name +
"_x0", type=
"D", doc=
"Trail head X coordinate.", units=
"pixel")
90 self.
keyY0keyY0 = schema.addField(name +
"_y0", type=
"D", doc=
"Trail head Y coordinate.", units=
"pixel")
91 self.
keyX1keyX1 = schema.addField(name +
"_x1", type=
"D", doc=
"Trail tail X coordinate.", units=
"pixel")
92 self.
keyY1keyY1 = schema.addField(name +
"_y1", type=
"D", doc=
"Trail tail Y coordinate.", units=
"pixel")
93 self.
keyFluxkeyFlux = schema.addField(name +
"_flux", type=
"D", doc=
"Trailed source flux.", units=
"count")
94 self.
keyLkeyL = schema.addField(name +
"_length", type=
"D", doc=
"Trail length.", units=
"pixel")
95 self.
keyAnglekeyAngle = schema.addField(name +
"_angle", type=
"D", doc=
"Angle measured from +x-axis.")
98 self.
keyX0ErrkeyX0Err = schema.addField(name +
"_x0Err", type=
"D",
99 doc=
"Trail head X coordinate error.", units=
"pixel")
100 self.
keyY0ErrkeyY0Err = schema.addField(name +
"_y0Err", type=
"D",
101 doc=
"Trail head Y coordinate error.", units=
"pixel")
102 self.
keyX1ErrkeyX1Err = schema.addField(name +
"_x1Err", type=
"D",
103 doc=
"Trail tail X coordinate error.", units=
"pixel")
104 self.
keyY1ErrkeyY1Err = schema.addField(name +
"_y1Err", type=
"D",
105 doc=
"Trail tail Y coordinate error.", units=
"pixel")
108 flagDefs.addFailureFlag(
"No trailed-source measured")
109 self.
NO_FLUXNO_FLUX = flagDefs.add(
"flag_noFlux",
"No suitable prior flux measurement")
110 self.
NO_CONVERGENO_CONVERGE = flagDefs.add(
"flag_noConverge",
"The root finder did not converge")
111 self.
flagHandlerflagHandler = FlagHandler.addFields(schema, name, flagDefs)
116 """Run the Naive trailed source measurement algorithm.
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.
127 lsst.meas.base.SingleFramePlugin.measure
130 Ixx, Iyy, Ixy = measRecord.getShape().getParameterVector()
135 a2 = 0.5 * (xpy + np.sqrt(xmy2 + 4.0*xy2))
136 sigma = exposure.getPsf().getSigma()
138 length, results = self.
findLengthfindLength(a2, sigma*sigma)
139 if not results.converged:
140 lsst.log.info(results.flag)
143 theta = 0.5 * np.arctan2(2.0 * Ixy, xmy)
145 dydt = a*np.cos(theta)
146 dxdt = a*np.sin(theta)
153 flux = measRecord.get(
"base_SdssShape_instFlux")
156 if not np.isfinite(flux):
157 if np.isfinite(measRecord.getApInstFlux()):
158 flux = measRecord.getApInstFlux()
163 xcErr2, ycErr2 = np.diag(measRecord.getCentroidErr())
164 IxxErr2, IyyErr2, IxyErr2 = np.diag(measRecord.getShapeErr())
165 desc = np.sqrt(xmy2 + 4.0*xy2)
166 denom = 2*np.sqrt(2.0*(Ixx + np.sqrt(4.0*xy2 + xmy2 + Iyy)))
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)
175 xErr2 = aErr2*dxda*dxda + thetaErr2*dxdt*dxdt
176 yErr2 = aErr2*dyda*dyda + thetaErr2*dydt*dydt
177 x0Err = np.sqrt(xErr2 + xcErr2)
178 y0Err = np.sqrt(yErr2 + ycErr2)
181 measRecord.set(self.
keyX0keyX0, x0)
182 measRecord.set(self.
keyY0keyY0, y0)
183 measRecord.set(self.
keyX1keyX1, x1)
184 measRecord.set(self.
keyY1keyY1, y1)
185 measRecord.set(self.
keyFluxkeyFlux, flux)
186 measRecord.set(self.
keyLkeyL, length)
187 measRecord.set(self.
keyAnglekeyAngle, theta)
188 measRecord.set(self.
keyX0ErrkeyX0Err, x0Err)
189 measRecord.set(self.
keyY0ErrkeyY0Err, y0Err)
190 measRecord.set(self.
keyX1ErrkeyX1Err, x0Err)
191 measRecord.set(self.
keyY1ErrkeyY1Err, y0Err)
193 def fail(self, measRecord, error=None):
198 lsst.meas.base.SingleFramePlugin.fail
201 self.
flagHandlerflagHandler.handleFailure(measRecord)
203 self.
flagHandlerflagHandler.handleFailure(measRecord, error.cpp)
205 def _computeSecondMomentDiff(self, z, c):
206 """Compute difference of the numerical and analytic second moments.
211 Proportional to the length of the trail. (see notes)
218 Difference in numerical and analytic second moments.
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
230 diff = erf(z) - c*z*np.exp(-z*z)
234 """Find the length of a trail, given adaptive second-moments.
236 Uses a root finder to compute the length of a trail corresponding to
237 the adaptive second-moments computed by previous measurements
243 Adaptive second-moment along x-axis.
245 Adaptive second-moment along y-axis.
251 results : `scipy.optimize.RootResults`
252 Contains messages about convergence from the root finder.
256 c = 4.0*Ixx/(xpy*np.sqrt(np.pi))
263 0.001, 1.0, full_output=
True)
265 length = 2.0*z*np.sqrt(2.0*xpy)
266 return length, results
vector-type utility class to build a collection of FlagDefinitions
Exception to be thrown when a measurement algorithm experiences a known failure mode.
def _computeSecondMomentDiff(self, z, c)
def getExecutionOrder(cls)
def findLength(self, Ixx, Iyy)
def __init__(self, config, name, schema, metadata)
def measure(self, measRecord, exposure)
def fail(self, measRecord, error=None)