25 import scipy.optimize
as sciOpt
26 from scipy.special
import erf
31 from lsst.meas.base import SingleFramePlugin, SingleFramePluginConfig
32 from lsst.meas.base import FlagHandler, FlagDefinitionList, SafeCentroidExtractor
35 __all__ = (
"SingleFrameNaiveTrailConfig",
"SingleFrameNaiveTrailPlugin")
39 """Config class for SingleFrameNaiveTrailPlugin.
44 @register(
"ext_trailedSources_Naive")
46 """Naive trailed source measurement plugin
48 Measures the length, angle from +x-axis, and end points of an extended
49 source using the second moments.
53 config: `SingleFrameNaiveTrailConfig`
57 schema: `lsst.afw.table.Schema`
58 Schema for the output catalog.
59 metadata: `lsst.daf.base.PropertySet`
60 Metadata to be attached to output catalog.
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.
75 lsst.meas.base.SingleFramePlugin
78 ConfigClass = SingleFrameNaiveTrailConfig
84 return cls.APCORR_ORDER + 0.1
86 def __init__(self, config, name, schema, metadata):
87 super().
__init__(config, name, schema, metadata)
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.")
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")
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.
flagHandlerflagHandler = FlagHandler.addFields(schema, name, flagDefs)
119 """Run the Naive trailed source measurement algorithm.
123 measRecord : `lsst.afw.table.SourceRecord`
124 Record describing the object being measured.
125 exposure : `lsst.afw.image.Exposure`
126 Pixel data to be measured.
130 lsst.meas.base.SingleFramePlugin.measure
134 xc = measRecord.get(
"base_SdssShape_x")
135 yc = measRecord.get(
"base_SdssShape_y")
136 if not np.isfinite(xc)
or not np.isfinite(yc):
139 ra, dec = self.
computeRaDeccomputeRaDec(exposure, xc, yc)
141 Ixx, Iyy, Ixy = measRecord.getShape().getParameterVector()
146 a2 = 0.5 * (xpy + np.sqrt(xmy2 + 4.0*xy2))
147 sigma = exposure.getPsf().getSigma()
149 length, results = self.
findLengthfindLength(a2, sigma*sigma)
150 if not results.converged:
151 lsst.log.info(results.flag)
154 theta = 0.5 * np.arctan2(2.0 * Ixy, xmy)
156 dydt = a*np.cos(theta)
157 dxdt = a*np.sin(theta)
164 flux = measRecord.get(
"base_SdssShape_instFlux")
167 if not np.isfinite(flux):
168 if np.isfinite(measRecord.getApInstFlux()):
169 flux = measRecord.getApInstFlux()
174 xcErr2, ycErr2 = np.diag(measRecord.getCentroidErr())
175 IxxErr2, IyyErr2, IxyErr2 = np.diag(measRecord.getShapeErr())
176 desc = np.sqrt(xmy2 + 4.0*xy2)
177 denom = 2*np.sqrt(2.0*(Ixx + np.sqrt(4.0*xy2 + xmy2 + Iyy)))
178 dadIxx = (1.0 + (xmy/desc)) / denom
179 dadIyy = (1.0 - (xmy/desc)) / denom
180 dadIxy = (4.0*Ixy) / (desc * denom)
181 aErr2 = IxxErr2*dadIxx*dadIxx + IyyErr2*dadIyy*dadIyy + IxyErr2*dadIxy*dadIxy
182 thetaErr2 = ((IxxErr2 + IyyErr2)*xy2 + xmy2*IxyErr2) / (desc*desc*desc*desc)
186 xErr2 = aErr2*dxda*dxda + thetaErr2*dxdt*dxdt
187 yErr2 = aErr2*dyda*dyda + thetaErr2*dydt*dydt
188 x0Err = np.sqrt(xErr2 + xcErr2)
189 y0Err = np.sqrt(yErr2 + ycErr2)
192 measRecord.set(self.
keyRakeyRa, ra)
193 measRecord.set(self.
keyDeckeyDec, dec)
194 measRecord.set(self.
keyX0keyX0, x0)
195 measRecord.set(self.
keyY0keyY0, y0)
196 measRecord.set(self.
keyX1keyX1, x1)
197 measRecord.set(self.
keyY1keyY1, y1)
198 measRecord.set(self.
keyFluxkeyFlux, flux)
199 measRecord.set(self.
keyLkeyL, length)
200 measRecord.set(self.
keyAnglekeyAngle, theta)
201 measRecord.set(self.
keyX0ErrkeyX0Err, x0Err)
202 measRecord.set(self.
keyY0ErrkeyY0Err, y0Err)
203 measRecord.set(self.
keyX1ErrkeyX1Err, x0Err)
204 measRecord.set(self.
keyY1ErrkeyY1Err, y0Err)
206 def fail(self, measRecord, error=None):
211 lsst.meas.base.SingleFramePlugin.fail
214 self.
flagHandlerflagHandler.handleFailure(measRecord)
216 self.
flagHandlerflagHandler.handleFailure(measRecord, error.cpp)
218 def _computeSecondMomentDiff(self, z, c):
219 """Compute difference of the numerical and analytic second moments.
224 Proportional to the length of the trail. (see notes)
231 Difference in numerical and analytic second moments.
235 This is a simplified expression for the difference between the stack
236 computed adaptive second-moment and the analytic solution. The variable
237 z is proportional to the length such that L = 2*z*sqrt(2*(Ixx+Iyy)),
238 and c is a constant (c = 4*Ixx/((Ixx+Iyy)*sqrt(pi))). Both have been
239 defined to avoid unnecessary floating-point operations in the root
243 diff = erf(z) - c*z*np.exp(-z*z)
247 """Find the length of a trail, given adaptive second-moments.
249 Uses a root finder to compute the length of a trail corresponding to
250 the adaptive second-moments computed by previous measurements
256 Adaptive second-moment along x-axis.
258 Adaptive second-moment along y-axis.
264 results : `scipy.optimize.RootResults`
265 Contains messages about convergence from the root finder.
269 c = 4.0*Ixx/(xpy*np.sqrt(np.pi))
276 0.001, 1.0, full_output=
True)
278 length = 2.0*z*np.sqrt(2.0*xpy)
279 return length, results
282 """Convert pixel coordinates to RA and Dec.
286 exposure : `lsst.afw.image.ExposureF`
287 Exposure object containing the WCS.
289 x coordinate of the trail centroid
291 y coodinate of the trail centroid
301 wcs = exposure.getWcs()
302 center = wcs.pixelToSky(
Point2D(x, y))
303 ra = center.getRa().asDegrees()
304 dec = center.getDec().asDegrees()
def _computeSecondMomentDiff(self, z, c)
def computeRaDec(self, exposure, x, y)
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)