25import scipy.optimize
as sciOpt
26from scipy.special
import erf
31from lsst.meas.base import SingleFramePlugin, SingleFramePluginConfig
32from lsst.meas.base import FlagHandler, FlagDefinitionList, SafeCentroidExtractor
35from ._trailedSources
import VeresModel
36from .utils
import getMeasurementCutout
38__all__ = (
"SingleFrameNaiveTrailConfig",
"SingleFrameNaiveTrailPlugin")
42 """Config class for SingleFrameNaiveTrailPlugin.
47@register("ext_trailedSources_Naive")
49 """Naive trailed source measurement plugin
51 Measures the length, angle from +x-axis,
and end points of an extended
52 source using the second moments.
56 config: `SingleFrameNaiveTrailConfig`
61 Schema
for the output catalog.
63 Metadata to be attached to output catalog.
67 This measurement plugin aims to utilize the already measured adaptive
68 second moments to naively estimate the length
and angle,
and thus
69 end-points, of a fast-moving, trailed source. The length
is solved
for via
70 finding the root of the difference between the numerical (stack computed)
71 and the analytic adaptive second moments. The angle, theta,
from the x-axis
72 is also computed via adaptive moments: theta = arctan(2*Ixy/(Ixx - Iyy))/2.
73 The end points of the trail are then given by (xc +/- (length/2)*cos(theta)
74 and yc +/- (length/2)*sin(theta)),
with xc
and yc being the centroid
79 lsst.meas.base.SingleFramePlugin
82 ConfigClass = SingleFrameNaiveTrailConfig
88 return cls.APCORR_ORDER + 0.1
90 def __init__(self, config, name, schema, metadata):
91 super().
__init__(config, name, schema, metadata)
94 self.
keyRa = schema.addField(name +
"_ra", type=
"D", doc=
"Trail centroid right ascension.")
95 self.
keyDec = schema.addField(name +
"_dec", type=
"D", doc=
"Trail centroid declination.")
96 self.
keyX0 = schema.addField(name +
"_x0", type=
"D", doc=
"Trail head X coordinate.", units=
"pixel")
97 self.
keyY0 = schema.addField(name +
"_y0", type=
"D", doc=
"Trail head Y coordinate.", units=
"pixel")
98 self.
keyX1 = schema.addField(name +
"_x1", type=
"D", doc=
"Trail tail X coordinate.", units=
"pixel")
99 self.
keyY1 = schema.addField(name +
"_y1", type=
"D", doc=
"Trail tail Y coordinate.", units=
"pixel")
100 self.
keyFlux = schema.addField(name +
"_flux", type=
"D", doc=
"Trailed source flux.", units=
"count")
101 self.
keyLength = schema.addField(name +
"_length", type=
"D", doc=
"Trail length.", units=
"pixel")
102 self.
keyAngle = schema.addField(name +
"_angle", type=
"D", doc=
"Angle measured from +x-axis.")
105 self.
keyX0Err = schema.addField(name +
"_x0Err", type=
"D",
106 doc=
"Trail head X coordinate error.", units=
"pixel")
107 self.
keyY0Err = schema.addField(name +
"_y0Err", type=
"D",
108 doc=
"Trail head Y coordinate error.", units=
"pixel")
109 self.
keyX1Err = schema.addField(name +
"_x1Err", type=
"D",
110 doc=
"Trail tail X coordinate error.", units=
"pixel")
111 self.
keyY1Err = schema.addField(name +
"_y1Err", type=
"D",
112 doc=
"Trail tail Y coordinate error.", units=
"pixel")
113 self.
keyFluxErr = schema.addField(name +
"_fluxErr", type=
"D",
114 doc=
"Trail flux error.", units=
"count")
116 doc=
"Trail length error.", units=
"pixel")
117 self.
keyAngleErr = schema.addField(name +
"_angleErr", type=
"D", doc=
"Trail angle error.")
120 flagDefs.addFailureFlag(
"No trailed-source measured")
121 self.
NO_FLUX = flagDefs.add(
"flag_noFlux",
"No suitable prior flux measurement")
122 self.
NO_CONVERGE = flagDefs.add(
"flag_noConverge",
"The root finder did not converge")
123 self.
NO_SIGMA = flagDefs.add(
"flag_noSigma",
"No PSF width (sigma)")
124 self.
SAFE_CENTROID = flagDefs.add(
"flag_safeCentroid",
"Fell back to safe centroid extractor")
130 """Run the Naive trailed source measurement algorithm.
135 Record describing the object being measured.
137 Pixel data to be measured.
141 lsst.meas.base.SingleFramePlugin.measure
146 xc = measRecord.get(
"base_SdssShape_x")
147 yc = measRecord.get(
"base_SdssShape_y")
148 if not np.isfinite(xc)
or not np.isfinite(yc):
149 xc, yc = self.centroidExtractor(measRecord, self.
flagHandler)
155 Ixx, Iyy, Ixy = measRecord.getShape().getParameterVector()
160 a2 = 0.5 * (xpy + np.sqrt(xmy2 + 4.0*xy2))
161 b2 = 0.5 * (xpy - np.sqrt(xmy2 + 4.0*xy2))
165 if measRecord.get(
"base_SdssShape_flag_unweighted"):
166 lsst.log.debug(
"Unweighed")
169 lsst.log.debug(
"Weighted")
170 length, gradLength, results = self.
findLength(a2, b2)
171 if not results.converged:
172 lsst.log.info(results.flag)
176 theta = 0.5 * np.arctan2(2.0 * Ixy, xmy)
180 dydtheta = radius*np.cos(theta)
181 dxdtheta = radius*np.sin(theta)
191 params = np.array([xc, yc, 1.0, length, theta])
192 model = VeresModel(cutout)
193 flux, gradFlux = model.computeFluxWithGradient(params)
196 if not np.isfinite(flux):
197 if np.isfinite(measRecord.getApInstFlux()):
198 flux = measRecord.getApInstFlux()
203 IxxErr2, IyyErr2, IxyErr2 = np.diag(measRecord.getShapeErr())
207 xcErr2, ycErr2 = np.diag(measRecord.getCentroidErr())
210 desc = np.sqrt(xmy2 + 4.0*xy2)
211 da2dIxx = 0.5*(1.0 + (xmy/desc))
212 da2dIyy = 0.5*(1.0 - (xmy/desc))
213 da2dIxy = 2.0*Ixy / desc
214 a2Err2 = IxxErr2*da2dIxx*da2dIxx + IyyErr2*da2dIyy*da2dIyy + IxyErr2*da2dIxy*da2dIxy
215 b2Err2 = IxxErr2*da2dIyy*da2dIyy + IyyErr2*da2dIxx*da2dIxx + IxyErr2*da2dIxy*da2dIxy
216 dLda2, dLdb2 = gradLength
217 lengthErr = np.sqrt(dLda2*dLda2*a2Err2 + dLdb2*dLdb2*b2Err2)
220 dThetadIxx = -Ixy / (xmy2 + 4.0*xy2)
221 dThetadIxy = xmy / (xmy2 + 4.0*xy2)
222 thetaErr = np.sqrt(dThetadIxx*dThetadIxx*(IxxErr2 + IyyErr2) + dThetadIxy*dThetadIxy*IxyErr2)
225 dFdxc, dFdyc, _, dFdL, dFdTheta = gradFlux
226 fluxErr = np.sqrt(dFdL*dFdL*lengthErr*lengthErr + dFdTheta*dFdTheta*thetaErr*thetaErr
227 + dFdxc*dFdxc*xcErr2 + dFdyc*dFdyc*ycErr2)
230 dxdradius = np.cos(theta)
231 dydradius = np.sin(theta)
232 radiusErr2 = lengthErr*lengthErr/4.0
233 xErr2 = np.sqrt(xcErr2 + radiusErr2*dxdradius*dxdradius + thetaErr*thetaErr*dxdtheta*dxdtheta)
234 yErr2 = np.sqrt(ycErr2 + radiusErr2*dydradius*dydradius + thetaErr*thetaErr*dydtheta*dydtheta)
235 x0Err = np.sqrt(xErr2)
236 y0Err = np.sqrt(yErr2)
239 measRecord.set(self.
keyRa, ra)
240 measRecord.set(self.
keyDec, dec)
241 measRecord.set(self.
keyX0, x0)
242 measRecord.set(self.
keyY0, y0)
243 measRecord.set(self.
keyX1, x1)
244 measRecord.set(self.
keyY1, y1)
245 measRecord.set(self.
keyFlux, flux)
247 measRecord.set(self.
keyAngle, theta)
248 measRecord.set(self.
keyX0Err, x0Err)
249 measRecord.set(self.
keyY0Err, y0Err)
250 measRecord.set(self.
keyX1Err, x0Err)
251 measRecord.set(self.
keyY1Err, y0Err)
256 def fail(self, measRecord, error=None):
261 lsst.meas.base.SingleFramePlugin.fail
266 self.
flagHandler.handleFailure(measRecord, error.cpp)
269 def _computeSecondMomentDiff(z, c):
270 """Compute difference of the numerical and analytic second moments.
275 Proportional to the length of the trail. (see notes)
282 Difference in numerical
and analytic second moments.
286 This
is a simplified expression
for the difference between the stack
287 computed adaptive second-moment
and the analytic solution. The variable
288 z
is proportional to the length such that length=2*z*sqrt(2*(Ixx+Iyy)),
289 and c
is a constant (c = 4*Ixx/((Ixx+Iyy)*sqrt(pi))). Both have been
290 defined to avoid unnecessary floating-point operations
in the root
294 diff = erf(z) - c*z*np.exp(-z*z)
299 """Find the length of a trail, given adaptive second-moments.
301 Uses a root finder to compute the length of a trail corresponding to
302 the adaptive second-moments computed by previous measurements
308 Adaptive second-moment along x-axis.
310 Adaptive second-moment along y-axis.
316 results : `scipy.optimize.RootResults`
317 Contains messages about convergence from the root finder.
321 c = 4.0*Ixx/(xpy*np.sqrt(np.pi))
328 0.001, 1.0, full_output=
True)
330 length = 2.0*z*np.sqrt(2.0*xpy)
332 return length, gradLength, results
335 def _gradFindLength(Ixx, Iyy, z, c):
336 """Compute the gradient of the findLength function.
344 fac = 4.0 / (spi*xpy2)
350 dzdf = spi / (enz2*(spi*c*(2.0*z*z - 1.0) + 2.0))
352 dLdz = 2.0*np.sqrt(2.0)*sxpy
353 pLpIxx = np.sqrt(2.0)*z / sxpy
355 dLdc = dLdz*dzdf*dfdc
356 dLdIxx = dLdc*dcdIxx + pLpIxx
357 dLdIyy = dLdc*dcdIyy + pLpIxx
358 return dLdIxx, dLdIyy
362 """Compute the length of a trail, given unweighted second-moments.
364 denom = np.sqrt(Ixx - 2.0*Iyy)
366 length = np.sqrt(6.0)*denom
368 dLdIxx = np.sqrt(1.5) / denom
369 dLdIyy = -np.sqrt(6.0) / denom
370 return length, (dLdIxx, dLdIyy)
374 """Convert pixel coordinates to RA and Dec.
378 exposure : `lsst.afw.image.ExposureF`
379 Exposure object containing the WCS.
381 x coordinate of the trail centroid
383 y coodinate of the trail centroid
393 wcs = exposure.getWcs()
394 center = wcs.pixelToSky(Point2D(x, y))
395 ra = center.getRa().asDegrees()
396 dec = center.getDec().asDegrees()
def computeRaDec(exposure, x, y)
def getExecutionOrder(cls)
def computeLength(Ixx, Iyy)
def _computeSecondMomentDiff(z, c)
def __init__(self, config, name, schema, metadata)
def _gradFindLength(Ixx, Iyy, z, c)
def findLength(cls, Ixx, Iyy)
def measure(self, measRecord, exposure)
def fail(self, measRecord, error=None)
def getMeasurementCutout(measRecord, exposure)