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
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.
NO_SIGMANO_SIGMA = flagDefs.add(
"flag_noSigma",
"No PSF width (sigma)")
115 self.
flagHandlerflagHandler = FlagHandler.addFields(schema, name, flagDefs)
120 """Run the Naive trailed source measurement algorithm.
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.
131 lsst.meas.base.SingleFramePlugin.measure
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):
140 ra, dec = self.
computeRaDeccomputeRaDec(exposure, xc, yc)
142 Ixx, Iyy, Ixy = measRecord.getShape().getParameterVector()
147 a2 = 0.5 * (xpy + np.sqrt(xmy2 + 4.0*xy2))
151 sigma = exposure.getPsf().computeShape(center).getTraceRadius()
152 if not np.isfinite(sigma):
155 length, results = self.
findLengthfindLength(a2, sigma*sigma)
156 if not results.converged:
157 lsst.log.info(results.flag)
160 theta = 0.5 * np.arctan2(2.0 * Ixy, xmy)
162 dydt = a*np.cos(theta)
163 dxdt = a*np.sin(theta)
170 flux = measRecord.get(
"base_SdssShape_instFlux")
173 if not np.isfinite(flux):
174 if np.isfinite(measRecord.getApInstFlux()):
175 flux = measRecord.getApInstFlux()
180 xcErr2, ycErr2 = np.diag(measRecord.getCentroidErr())
181 IxxErr2, IyyErr2, IxyErr2 = np.diag(measRecord.getShapeErr())
182 desc = np.sqrt(xmy2 + 4.0*xy2)
183 denom = 2*np.sqrt(2.0*(Ixx + np.sqrt(4.0*xy2 + xmy2 + Iyy)))
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)
192 xErr2 = aErr2*dxda*dxda + thetaErr2*dxdt*dxdt
193 yErr2 = aErr2*dyda*dyda + thetaErr2*dydt*dydt
194 x0Err = np.sqrt(xErr2 + xcErr2)
195 y0Err = np.sqrt(yErr2 + ycErr2)
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)
212 def fail(self, measRecord, error=None):
217 lsst.meas.base.SingleFramePlugin.fail
220 self.
flagHandlerflagHandler.handleFailure(measRecord)
222 self.
flagHandlerflagHandler.handleFailure(measRecord, error.cpp)
224 def _computeSecondMomentDiff(self, z, c):
225 """Compute difference of the numerical and analytic second moments.
230 Proportional to the length of the trail. (see notes)
237 Difference in numerical and analytic second moments.
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
249 diff = erf(z) - c*z*np.exp(-z*z)
253 """Find the length of a trail, given adaptive second-moments.
255 Uses a root finder to compute the length of a trail corresponding to
256 the adaptive second-moments computed by previous measurements
262 Adaptive second-moment along x-axis.
264 Adaptive second-moment along y-axis.
270 results : `scipy.optimize.RootResults`
271 Contains messages about convergence from the root finder.
275 c = 4.0*Ixx/(xpy*np.sqrt(np.pi))
282 0.001, 1.0, full_output=
True)
284 length = 2.0*z*np.sqrt(2.0*xpy)
285 return length, results
288 """Convert pixel coordinates to RA and Dec.
292 exposure : `lsst.afw.image.ExposureF`
293 Exposure object containing the WCS.
295 x coordinate of the trail centroid
297 y coodinate of the trail centroid
307 wcs = exposure.getWcs()
308 center = wcs.pixelToSky(
Point2D(x, y))
309 ra = center.getRa().asDegrees()
310 dec = center.getDec().asDegrees()
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 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)
Point< double, 2 > Point2D