LSST Applications  21.0.0-172-gfb10e10a+18fedfabac,22.0.0+297cba6710,22.0.0+80564b0ff1,22.0.0+8d77f4f51a,22.0.0+a28f4c53b1,22.0.0+dcf3732eb2,22.0.1-1-g7d6de66+2a20fdde0d,22.0.1-1-g8e32f31+297cba6710,22.0.1-1-geca5380+7fa3b7d9b6,22.0.1-12-g44dc1dc+2a20fdde0d,22.0.1-15-g6a90155+515f58c32b,22.0.1-16-g9282f48+790f5f2caa,22.0.1-2-g92698f7+dcf3732eb2,22.0.1-2-ga9b0f51+7fa3b7d9b6,22.0.1-2-gd1925c9+bf4f0e694f,22.0.1-24-g1ad7a390+a9625a72a8,22.0.1-25-g5bf6245+3ad8ecd50b,22.0.1-25-gb120d7b+8b5510f75f,22.0.1-27-g97737f7+2a20fdde0d,22.0.1-32-gf62ce7b1+aa4237961e,22.0.1-4-g0b3f228+2a20fdde0d,22.0.1-4-g243d05b+871c1b8305,22.0.1-4-g3a563be+32dcf1063f,22.0.1-4-g44f2e3d+9e4ab0f4fa,22.0.1-42-gca6935d93+ba5e5ca3eb,22.0.1-5-g15c806e+85460ae5f3,22.0.1-5-g58711c4+611d128589,22.0.1-5-g75bb458+99c117b92f,22.0.1-6-g1c63a23+7fa3b7d9b6,22.0.1-6-g50866e6+84ff5a128b,22.0.1-6-g8d3140d+720564cf76,22.0.1-6-gd805d02+cc5644f571,22.0.1-8-ge5750ce+85460ae5f3,master-g6e05de7fdc+babf819c66,master-g99da0e417a+8d77f4f51a,w.2021.48
LSST Data Management Base Package
NaivePlugin.py
Go to the documentation of this file.
1 #
2 # This file is part of meas_extensions_trailedSources.
3 #
4 # Developed for the LSST Data Management System.
5 # This product includes software developed by the LSST Project
6 # (http://www.lsst.org).
7 # See the COPYRIGHT file at the top-level directory of this distribution
8 # for details of code ownership.
9 #
10 # This program is free software: you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation, either version 3 of the License, or
13 # (at your option) any later version.
14 #
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
19 #
20 # You should have received a copy of the GNU General Public License
21 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 #
23 
24 import numpy as np
25 import scipy.optimize as sciOpt
26 from scipy.special import erf
27 
28 import lsst.log
29 from lsst.geom import Point2D
30 from lsst.meas.base.pluginRegistry import register
31 from lsst.meas.base import SingleFramePlugin, SingleFramePluginConfig
32 from lsst.meas.base import FlagHandler, FlagDefinitionList, SafeCentroidExtractor
33 from lsst.meas.base import MeasurementError
34 
35 __all__ = ("SingleFrameNaiveTrailConfig", "SingleFrameNaiveTrailPlugin")
36 
37 
39  """Config class for SingleFrameNaiveTrailPlugin.
40  """
41  pass
42 
43 
44 @register("ext_trailedSources_Naive")
46  """Naive trailed source measurement plugin
47 
48  Measures the length, angle from +x-axis, and end points of an extended
49  source using the second moments.
50 
51  Parameters
52  ----------
53  config: `SingleFrameNaiveTrailConfig`
54  Plugin configuration.
55  name: `str`
56  Plugin name.
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.
61 
62  Notes
63  -----
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.
72 
73  See also
74  --------
75  lsst.meas.base.SingleFramePlugin
76  """
77 
78  ConfigClass = SingleFrameNaiveTrailConfig
79 
80  @classmethod
82  # Needs centroids, shape, and flux measurements.
83  # VeresPlugin is run after, which requires image data.
84  return cls.APCORR_ORDERAPCORR_ORDER + 0.1
85 
86  def __init__(self, config, name, schema, metadata):
87  super().__init__(config, name, schema, metadata)
88 
89  # Measurement Keys
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.")
99 
100  # Measurement Error Keys
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")
109 
110  flagDefs = FlagDefinitionList()
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)
116 
117  self.centriodExtractorcentriodExtractor = SafeCentroidExtractor(schema, name)
118 
119  def measure(self, measRecord, exposure):
120  """Run the Naive trailed source measurement algorithm.
121 
122  Parameters
123  ----------
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.
128 
129  See also
130  --------
131  lsst.meas.base.SingleFramePlugin.measure
132  """
133 
134  # Get the SdssShape centroid or fall back to slot
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):
138  xc, yc = self.centriodExtractorcentriodExtractor(measRecord, self.flagHandlerflagHandler)
139 
140  ra, dec = self.computeRaDeccomputeRaDec(exposure, xc, yc)
141 
142  Ixx, Iyy, Ixy = measRecord.getShape().getParameterVector()
143  xmy = Ixx - Iyy
144  xpy = Ixx + Iyy
145  xmy2 = xmy*xmy
146  xy2 = Ixy*Ixy
147  a2 = 0.5 * (xpy + np.sqrt(xmy2 + 4.0*xy2))
148 
149  # Get the width of the PSF at the center of the trail
150  center = Point2D(xc, yc)
151  sigma = exposure.getPsf().computeShape(center).getTraceRadius()
152  if not np.isfinite(sigma):
153  raise MeasurementError(self.NO_SIGMANO_SIGMA, self.NO_SIGMANO_SIGMA.number)
154 
155  length, results = self.findLengthfindLength(a2, sigma*sigma)
156  if not results.converged:
157  lsst.log.info(results.flag)
158  raise MeasurementError(self.NO_CONVERGENO_CONVERGE.doc, self.NO_CONVERGENO_CONVERGE.number)
159 
160  theta = 0.5 * np.arctan2(2.0 * Ixy, xmy)
161  a = length/2.0
162  dydt = a*np.cos(theta)
163  dxdt = a*np.sin(theta)
164  x0 = xc - dydt
165  y0 = yc - dxdt
166  x1 = xc + dydt
167  y1 = yc + dxdt
168 
169  # For now, use the shape flux.
170  flux = measRecord.get("base_SdssShape_instFlux")
171 
172  # Fall back to aperture flux
173  if not np.isfinite(flux):
174  if np.isfinite(measRecord.getApInstFlux()):
175  flux = measRecord.getApInstFlux()
176  else:
177  raise MeasurementError(self.NO_FLUXNO_FLUX.doc, self.NO_FLUXNO_FLUX.number)
178 
179  # Propagate errors from second moments
180  xcErr2, ycErr2 = np.diag(measRecord.getCentroidErr())
181  IxxErr2, IyyErr2, IxyErr2 = np.diag(measRecord.getShapeErr())
182  desc = np.sqrt(xmy2 + 4.0*xy2) # Descriminant^1/2 of EV equation
183  denom = 2*np.sqrt(2.0*(Ixx + np.sqrt(4.0*xy2 + xmy2 + Iyy))) # Denominator for dadIxx and dadIyy
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)
189 
190  dxda = np.cos(theta)
191  dyda = np.sin(theta)
192  xErr2 = aErr2*dxda*dxda + thetaErr2*dxdt*dxdt
193  yErr2 = aErr2*dyda*dyda + thetaErr2*dydt*dydt
194  x0Err = np.sqrt(xErr2 + xcErr2) # Same for x1
195  y0Err = np.sqrt(yErr2 + ycErr2) # Same for y1
196 
197  # Set flags
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)
211 
212  def fail(self, measRecord, error=None):
213  """Record failure
214 
215  See also
216  --------
217  lsst.meas.base.SingleFramePlugin.fail
218  """
219  if error is None:
220  self.flagHandlerflagHandler.handleFailure(measRecord)
221  else:
222  self.flagHandlerflagHandler.handleFailure(measRecord, error.cpp)
223 
224  def _computeSecondMomentDiff(self, z, c):
225  """Compute difference of the numerical and analytic second moments.
226 
227  Parameters
228  ----------
229  z : `float`
230  Proportional to the length of the trail. (see notes)
231  c : `float`
232  Constant (see notes)
233 
234  Returns
235  -------
236  diff : `float`
237  Difference in numerical and analytic second moments.
238 
239  Notes
240  -----
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
246  finder.
247  """
248 
249  diff = erf(z) - c*z*np.exp(-z*z)
250  return diff
251 
252  def findLength(self, Ixx, Iyy):
253  """Find the length of a trail, given adaptive second-moments.
254 
255  Uses a root finder to compute the length of a trail corresponding to
256  the adaptive second-moments computed by previous measurements
257  (ie. SdssShape).
258 
259  Parameters
260  ----------
261  Ixx : `float`
262  Adaptive second-moment along x-axis.
263  Iyy : `float`
264  Adaptive second-moment along y-axis.
265 
266  Returns
267  -------
268  length : `float`
269  Length of the trail.
270  results : `scipy.optimize.RootResults`
271  Contains messages about convergence from the root finder.
272  """
273 
274  xpy = Ixx + Iyy
275  c = 4.0*Ixx/(xpy*np.sqrt(np.pi))
276 
277  # Given a 'c' in (c_min, c_max], the root is contained in (0,1].
278  # c_min is given by the case: Ixx == Iyy, ie. a point source.
279  # c_max is given by the limit Ixx >> Iyy.
280  # Emperically, 0.001 is a suitable lower bound, assuming Ixx > Iyy.
281  z, results = sciOpt.brentq(lambda z: self._computeSecondMomentDiff_computeSecondMomentDiff(z, c),
282  0.001, 1.0, full_output=True)
283 
284  length = 2.0*z*np.sqrt(2.0*xpy)
285  return length, results
286 
287  def computeRaDec(self, exposure, x, y):
288  """Convert pixel coordinates to RA and Dec.
289 
290  Parameters
291  ----------
292  exposure : `lsst.afw.image.ExposureF`
293  Exposure object containing the WCS.
294  x : `float`
295  x coordinate of the trail centroid
296  y : `float`
297  y coodinate of the trail centroid
298 
299  Returns
300  -------
301  ra : `float`
302  Right ascension.
303  dec : `float`
304  Declination.
305  """
306 
307  wcs = exposure.getWcs()
308  center = wcs.pixelToSky(Point2D(x, y))
309  ra = center.getRa().asDegrees()
310  dec = center.getDec().asDegrees()
311  return ra, dec
vector-type utility class to build a collection of FlagDefinitions
Definition: FlagHandler.h:60
Exception to be thrown when a measurement algorithm experiences a known failure mode.
Definition: exceptions.h:48
Utility class for measurement algorithms that extracts a position from the Centroid slot and handles ...
Point< double, 2 > Point2D
Definition: Point.h:324
Definition: Log.h:717