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.
keyRakeyRa = schema.addField(name +
"_ra", type=
"D", doc=
"Trail centroid right ascension.")
95 self.
keyDeckeyDec = schema.addField(name +
"_dec", type=
"D", doc=
"Trail centroid declination.")
96 self.
keyX0keyX0 = schema.addField(name +
"_x0", type=
"D", doc=
"Trail head X coordinate.", units=
"pixel")
97 self.
keyY0keyY0 = schema.addField(name +
"_y0", type=
"D", doc=
"Trail head Y coordinate.", units=
"pixel")
98 self.
keyX1keyX1 = schema.addField(name +
"_x1", type=
"D", doc=
"Trail tail X coordinate.", units=
"pixel")
99 self.
keyY1keyY1 = schema.addField(name +
"_y1", type=
"D", doc=
"Trail tail Y coordinate.", units=
"pixel")
100 self.
keyFluxkeyFlux = schema.addField(name +
"_flux", type=
"D", doc=
"Trailed source flux.", units=
"count")
101 self.
keyLengthkeyLength = schema.addField(name +
"_length", type=
"D", doc=
"Trail length.", units=
"pixel")
102 self.
keyAnglekeyAngle = schema.addField(name +
"_angle", type=
"D", doc=
"Angle measured from +x-axis.")
105 self.
keyX0ErrkeyX0Err = schema.addField(name +
"_x0Err", type=
"D",
106 doc=
"Trail head X coordinate error.", units=
"pixel")
107 self.
keyY0ErrkeyY0Err = schema.addField(name +
"_y0Err", type=
"D",
108 doc=
"Trail head Y coordinate error.", units=
"pixel")
109 self.
keyX1ErrkeyX1Err = schema.addField(name +
"_x1Err", type=
"D",
110 doc=
"Trail tail X coordinate error.", units=
"pixel")
111 self.
keyY1ErrkeyY1Err = schema.addField(name +
"_y1Err", type=
"D",
112 doc=
"Trail tail Y coordinate error.", units=
"pixel")
115 flagDefs.addFailureFlag(
"No trailed-source measured")
116 self.
NO_FLUXNO_FLUX = flagDefs.add(
"flag_noFlux",
"No suitable prior flux measurement")
117 self.
NO_CONVERGENO_CONVERGE = flagDefs.add(
"flag_noConverge",
"The root finder did not converge")
118 self.
NO_SIGMANO_SIGMA = flagDefs.add(
"flag_noSigma",
"No PSF width (sigma)")
119 self.
flagHandlerflagHandler = FlagHandler.addFields(schema, name, flagDefs)
124 """Run the Naive trailed source measurement algorithm.
129 Record describing the object being measured.
131 Pixel data to be measured.
135 lsst.meas.base.SingleFramePlugin.measure
139 xc = measRecord.get(
"base_SdssShape_x")
140 yc = measRecord.get(
"base_SdssShape_y")
141 if not np.isfinite(xc)
or not np.isfinite(yc):
144 ra, dec = self.
computeRaDeccomputeRaDec(exposure, xc, yc)
146 Ixx, Iyy, Ixy = measRecord.getShape().getParameterVector()
151 a2 = 0.5 * (xpy + np.sqrt(xmy2 + 4.0*xy2))
154 center = Point2D(xc, yc)
155 sigma = exposure.getPsf().computeShape(center).getTraceRadius()
156 if not np.isfinite(sigma):
160 if measRecord.get(
"base_SdssShape_flag_unweighted"):
161 lsst.log.debug(
"Unweighed")
162 length = np.sqrt(6.0*(a2 - 2*sigma*sigma))
164 lsst.log.debug(
"Weighted")
165 length, results = self.
findLengthfindLength(a2, sigma*sigma)
166 if not results.converged:
167 lsst.log.info(results.flag)
170 theta = 0.5 * np.arctan2(2.0 * Ixy, xmy)
172 dydt = a*np.cos(theta)
173 dxdt = a*np.sin(theta)
184 params = np.array([xc, yc, 1.0, length, theta])
185 model = VeresModel(cutout)
186 modelArray = model.computeModelImage(params).array.flatten()
187 dataArray = cutout.image.array.flatten()
188 flux = np.dot(dataArray, modelArray) / np.dot(modelArray, modelArray)
191 if not np.isfinite(flux):
192 if np.isfinite(measRecord.getApInstFlux()):
193 flux = measRecord.getApInstFlux()
198 xcErr2, ycErr2 = np.diag(measRecord.getCentroidErr())
199 IxxErr2, IyyErr2, IxyErr2 = np.diag(measRecord.getShapeErr())
200 desc = np.sqrt(xmy2 + 4.0*xy2)
201 denom = 2*np.sqrt(2.0*(Ixx + np.sqrt(4.0*xy2 + xmy2 + Iyy)))
202 dadIxx = (1.0 + (xmy/desc)) / denom
203 dadIyy = (1.0 - (xmy/desc)) / denom
204 dadIxy = (4.0*Ixy) / (desc * denom)
205 aErr2 = IxxErr2*dadIxx*dadIxx + IyyErr2*dadIyy*dadIyy + IxyErr2*dadIxy*dadIxy
206 thetaErr2 = ((IxxErr2 + IyyErr2)*xy2 + xmy2*IxyErr2) / (desc*desc*desc*desc)
210 xErr2 = aErr2*dxda*dxda + thetaErr2*dxdt*dxdt
211 yErr2 = aErr2*dyda*dyda + thetaErr2*dydt*dydt
212 x0Err = np.sqrt(xErr2 + xcErr2)
213 y0Err = np.sqrt(yErr2 + ycErr2)
216 measRecord.set(self.
keyRakeyRa, ra)
217 measRecord.set(self.
keyDeckeyDec, dec)
218 measRecord.set(self.
keyX0keyX0, x0)
219 measRecord.set(self.
keyY0keyY0, y0)
220 measRecord.set(self.
keyX1keyX1, x1)
221 measRecord.set(self.
keyY1keyY1, y1)
222 measRecord.set(self.
keyFluxkeyFlux, flux)
223 measRecord.set(self.
keyLengthkeyLength, length)
224 measRecord.set(self.
keyAnglekeyAngle, theta)
225 measRecord.set(self.
keyX0ErrkeyX0Err, x0Err)
226 measRecord.set(self.
keyY0ErrkeyY0Err, y0Err)
227 measRecord.set(self.
keyX1ErrkeyX1Err, x0Err)
228 measRecord.set(self.
keyY1ErrkeyY1Err, y0Err)
230 def fail(self, measRecord, error=None):
235 lsst.meas.base.SingleFramePlugin.fail
238 self.
flagHandlerflagHandler.handleFailure(measRecord)
240 self.
flagHandlerflagHandler.handleFailure(measRecord, error.cpp)
242 def _computeSecondMomentDiff(self, z, c):
243 """Compute difference of the numerical and analytic second moments.
248 Proportional to the length of the trail. (see notes)
255 Difference in numerical
and analytic second moments.
259 This
is a simplified expression
for the difference between the stack
260 computed adaptive second-moment
and the analytic solution. The variable
261 z
is proportional to the length such that length=2*z*sqrt(2*(Ixx+Iyy)),
262 and c
is a constant (c = 4*Ixx/((Ixx+Iyy)*sqrt(pi))). Both have been
263 defined to avoid unnecessary floating-point operations
in the root
267 diff = erf(z) - c*z*np.exp(-z*z)
271 """Find the length of a trail, given adaptive second-moments.
273 Uses a root finder to compute the length of a trail corresponding to
274 the adaptive second-moments computed by previous measurements
280 Adaptive second-moment along x-axis.
282 Adaptive second-moment along y-axis.
288 results : `scipy.optimize.RootResults`
289 Contains messages about convergence from the root finder.
293 c = 4.0*Ixx/(xpy*np.sqrt(np.pi))
300 0.001, 1.0, full_output=
True)
302 length = 2.0*z*np.sqrt(2.0*xpy)
303 return length, results
306 """Convert pixel coordinates to RA and Dec.
310 exposure : `lsst.afw.image.ExposureF`
311 Exposure object containing the WCS.
313 x coordinate of the trail centroid
315 y coodinate of the trail centroid
325 wcs = exposure.getWcs()
326 center = wcs.pixelToSky(Point2D(x, y))
327 ra = center.getRa().asDegrees()
328 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)
def getMeasurementCutout(measRecord, exposure)