26import scipy.optimize
as sciOpt
27from scipy.special
import erf
31from lsst.meas.base import SingleFramePlugin, SingleFramePluginConfig
32from lsst.meas.base import FlagHandler, FlagDefinitionList, SafeCentroidExtractor
34from ._trailedSources
import VeresModel
35from .utils
import getMeasurementCutout
37__all__ = (
"SingleFrameNaiveTrailConfig",
"SingleFrameNaiveTrailPlugin")
41 """Config class for SingleFrameNaiveTrailPlugin.
46@register("ext_trailedSources_Naive")
48 """Naive trailed source measurement plugin
50 Measures the length, angle from +x-axis,
and end points of an extended
51 source using the second moments.
55 config: `SingleFrameNaiveTrailConfig`
60 Schema
for the output catalog.
62 Metadata to be attached to output catalog.
66 This measurement plugin aims to utilize the already measured adaptive
67 second moments to naively estimate the length
and angle,
and thus
68 end-points, of a fast-moving, trailed source. The length
is solved
for via
69 finding the root of the difference between the numerical (stack computed)
70 and the analytic adaptive second moments. The angle, theta,
from the x-axis
71 is also computed via adaptive moments: theta = arctan(2*Ixy/(Ixx - Iyy))/2.
72 The end points of the trail are then given by (xc +/- (length/2)*cos(theta)
73 and yc +/- (length/2)*sin(theta)),
with xc
and yc being the centroid
81 ConfigClass = SingleFrameNaiveTrailConfig
89 def __init__(self, config, name, schema, metadata, logName=None):
92 super().
__init__(config, name, schema, metadata, logName=logName)
95 self.
keyRa = schema.addField(name +
"_ra", type=
"D", doc=
"Trail centroid right ascension.")
96 self.
keyDec = schema.addField(name +
"_dec", type=
"D", doc=
"Trail centroid declination.")
97 self.
keyX0 = schema.addField(name +
"_x0", type=
"D", doc=
"Trail head X coordinate.", units=
"pixel")
98 self.
keyY0 = schema.addField(name +
"_y0", type=
"D", doc=
"Trail head Y coordinate.", units=
"pixel")
99 self.
keyX1 = schema.addField(name +
"_x1", type=
"D", doc=
"Trail tail X coordinate.", units=
"pixel")
100 self.
keyY1 = schema.addField(name +
"_y1", type=
"D", doc=
"Trail tail Y coordinate.", units=
"pixel")
101 self.
keyFlux = schema.addField(name +
"_flux", type=
"D", doc=
"Trailed source flux.", units=
"count")
102 self.
keyLength = schema.addField(name +
"_length", type=
"D", doc=
"Trail length.", units=
"pixel")
103 self.
keyAngle = schema.addField(name +
"_angle", type=
"D", doc=
"Angle measured from +x-axis.")
106 self.
keyX0Err = schema.addField(name +
"_x0Err", type=
"D",
107 doc=
"Trail head X coordinate error.", units=
"pixel")
108 self.
keyY0Err = schema.addField(name +
"_y0Err", type=
"D",
109 doc=
"Trail head Y coordinate error.", units=
"pixel")
110 self.
keyX1Err = schema.addField(name +
"_x1Err", type=
"D",
111 doc=
"Trail tail X coordinate error.", units=
"pixel")
112 self.
keyY1Err = schema.addField(name +
"_y1Err", type=
"D",
113 doc=
"Trail tail Y coordinate error.", units=
"pixel")
114 self.
keyFluxErr = schema.addField(name +
"_fluxErr", type=
"D",
115 doc=
"Trail flux error.", units=
"count")
117 doc=
"Trail length error.", units=
"pixel")
118 self.
keyAngleErr = schema.addField(name +
"_angleErr", type=
"D", doc=
"Trail angle error.")
121 self.
FAILURE = flagDefs.addFailureFlag(
"No trailed-source measured")
122 self.
NO_FLUX = flagDefs.add(
"flag_noFlux",
"No suitable prior flux measurement")
123 self.
NO_CONVERGE = flagDefs.add(
"flag_noConverge",
"The root finder did not converge")
124 self.
NO_SIGMA = flagDefs.add(
"flag_noSigma",
"No PSF width (sigma)")
125 self.
SAFE_CENTROID = flagDefs.add(
"flag_safeCentroid",
"Fell back to safe centroid extractor")
132 """Run the Naive trailed source measurement algorithm.
137 Record describing the object being measured.
139 Pixel data to be measured.
143 lsst.meas.base.SingleFramePlugin.measure
148 xc = measRecord.get(
"base_SdssShape_x")
149 yc = measRecord.get(
"base_SdssShape_y")
150 if not np.isfinite(xc)
or not np.isfinite(yc):
151 xc, yc = self.centroidExtractor(measRecord, self.
flagHandler)
159 Ixx, Iyy, Ixy = measRecord.getShape().getParameterVector()
164 a2 = 0.5 * (xpy + np.sqrt(xmy2 + 4.0*xy2))
165 b2 = 0.5 * (xpy - np.sqrt(xmy2 + 4.0*xy2))
169 if measRecord.get(
"base_SdssShape_flag_unweighted"):
170 self.
log.debug(
"Unweighted")
173 self.
log.debug(
"Weighted")
174 length, gradLength, results = self.
findLength(a2, b2)
175 if not results.converged:
176 self.
log.info(
"Results not converged: %s", results.flag)
182 theta = 0.5 * np.arctan2(2.0 * Ixy, xmy)
186 dydtheta = radius*np.cos(theta)
187 dxdtheta = radius*np.sin(theta)
194 cutout = getMeasurementCutout(measRecord, exposure)
197 params = np.array([xc, yc, 1.0, length, theta])
198 model = VeresModel(cutout)
199 flux, gradFlux = model.computeFluxWithGradient(params)
202 if not np.isfinite(flux):
203 if np.isfinite(measRecord.getApInstFlux()):
204 flux = measRecord.getApInstFlux()
211 IxxErr2, IyyErr2, IxyErr2 = np.diag(measRecord.getShapeErr())
215 xcErr2, ycErr2 = np.diag(measRecord.getCentroidErr())
218 desc = np.sqrt(xmy2 + 4.0*xy2)
219 da2dIxx = 0.5*(1.0 + (xmy/desc))
220 da2dIyy = 0.5*(1.0 - (xmy/desc))
221 da2dIxy = 2.0*Ixy / desc
222 a2Err2 = IxxErr2*da2dIxx*da2dIxx + IyyErr2*da2dIyy*da2dIyy + IxyErr2*da2dIxy*da2dIxy
223 b2Err2 = IxxErr2*da2dIyy*da2dIyy + IyyErr2*da2dIxx*da2dIxx + IxyErr2*da2dIxy*da2dIxy
224 dLda2, dLdb2 = gradLength
225 lengthErr = np.sqrt(dLda2*dLda2*a2Err2 + dLdb2*dLdb2*b2Err2)
228 dThetadIxx = -Ixy / (xmy2 + 4.0*xy2)
229 dThetadIxy = xmy / (xmy2 + 4.0*xy2)
230 thetaErr = np.sqrt(dThetadIxx*dThetadIxx*(IxxErr2 + IyyErr2) + dThetadIxy*dThetadIxy*IxyErr2)
233 dFdxc, dFdyc, _, dFdL, dFdTheta = gradFlux
234 fluxErr = np.sqrt(dFdL*dFdL*lengthErr*lengthErr + dFdTheta*dFdTheta*thetaErr*thetaErr
235 + dFdxc*dFdxc*xcErr2 + dFdyc*dFdyc*ycErr2)
238 dxdradius = np.cos(theta)
239 dydradius = np.sin(theta)
240 radiusErr2 = lengthErr*lengthErr/4.0
241 xErr2 = np.sqrt(xcErr2 + radiusErr2*dxdradius*dxdradius + thetaErr*thetaErr*dxdtheta*dxdtheta)
242 yErr2 = np.sqrt(ycErr2 + radiusErr2*dydradius*dydradius + thetaErr*thetaErr*dydtheta*dydtheta)
243 x0Err = np.sqrt(xErr2)
244 y0Err = np.sqrt(yErr2)
247 measRecord.set(self.
keyRa, ra)
248 measRecord.set(self.
keyDec, dec)
249 measRecord.set(self.
keyX0, x0)
250 measRecord.set(self.
keyY0, y0)
251 measRecord.set(self.
keyX1, x1)
252 measRecord.set(self.
keyY1, y1)
253 measRecord.set(self.
keyFlux, flux)
255 measRecord.set(self.
keyAngle, theta)
256 measRecord.set(self.
keyX0Err, x0Err)
257 measRecord.set(self.
keyY0Err, y0Err)
258 measRecord.set(self.
keyX1Err, x0Err)
259 measRecord.set(self.
keyY1Err, y0Err)
264 def fail(self, measRecord, error=None):
269 lsst.meas.base.SingleFramePlugin.fail
274 self.
flagHandler.handleFailure(measRecord, error.cpp)
277 def _computeSecondMomentDiff(z, c):
278 """Compute difference of the numerical and analytic second moments.
283 Proportional to the length of the trail. (see notes)
290 Difference in numerical
and analytic second moments.
294 This
is a simplified expression
for the difference between the stack
295 computed adaptive second-moment
and the analytic solution. The variable
296 z
is proportional to the length such that length=2*z*sqrt(2*(Ixx+Iyy)),
297 and c
is a constant (c = 4*Ixx/((Ixx+Iyy)*sqrt(pi))). Both have been
298 defined to avoid unnecessary floating-point operations
in the root
302 diff = erf(z) - c*z*np.exp(-z*z)
307 """Find the length of a trail, given adaptive second-moments.
309 Uses a root finder to compute the length of a trail corresponding to
310 the adaptive second-moments computed by previous measurements
316 Adaptive second-moment along x-axis.
318 Adaptive second-moment along y-axis.
324 results : `scipy.optimize.RootResults`
325 Contains messages about convergence from the root finder.
329 c = 4.0*Ixx/(xpy*np.sqrt(np.pi))
336 0.001, 1.0, full_output=
True)
338 length = 2.0*z*np.sqrt(2.0*xpy)
340 return length, gradLength, results
343 def _gradFindLength(Ixx, Iyy, z, c):
344 """Compute the gradient of the findLength function.
352 fac = 4.0 / (spi*xpy2)
358 dzdf = spi / (enz2*(spi*c*(2.0*z*z - 1.0) + 2.0))
360 dLdz = 2.0*np.sqrt(2.0)*sxpy
361 pLpIxx = np.sqrt(2.0)*z / sxpy
363 dLdc = dLdz*dzdf*dfdc
364 dLdIxx = dLdc*dcdIxx + pLpIxx
365 dLdIyy = dLdc*dcdIyy + pLpIxx
366 return dLdIxx, dLdIyy
370 """Compute the length of a trail, given unweighted second-moments.
372 denom = np.sqrt(Ixx - 2.0*Iyy)
374 length = np.sqrt(6.0)*denom
376 dLdIxx = np.sqrt(1.5) / denom
377 dLdIyy = -np.sqrt(6.0) / denom
378 return length, (dLdIxx, dLdIyy)
382 """Convert pixel coordinates to RA and Dec.
386 exposure : `lsst.afw.image.ExposureF`
387 Exposure object containing the WCS.
389 x coordinate of the trail centroid
391 y coodinate of the trail centroid
401 wcs = exposure.getWcs()
402 center = wcs.pixelToSky(Point2D(x, y))
403 ra = center.getRa().asDegrees()
404 dec = center.getDec().asDegrees()
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Defines the fields and offsets for a table.
Record class that contains measurements made on a single exposure.
Class for storing generic metadata.
vector-type utility class to build a collection of FlagDefinitions
def computeRaDec(exposure, x, y)
def getExecutionOrder(cls)
def computeLength(Ixx, Iyy)
def _computeSecondMomentDiff(z, c)
def __init__(self, config, name, schema, metadata, logName=None)
def _gradFindLength(Ixx, Iyy, z, c)
def findLength(cls, Ixx, Iyy)
def measure(self, measRecord, exposure)
def fail(self, measRecord, error=None)