lsst.meas.extensions.trailedSources  master-gf91b6b1793+e9a297bc6f
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 
38 class SingleFrameNaiveTrailConfig(SingleFramePluginConfig):
39  """Config class for SingleFrameNaiveTrailPlugin.
40  """
41  pass
42 
43 
44 @register("ext_trailedSources_Naive")
45 class SingleFrameNaiveTrailPlugin(SingleFramePlugin):
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_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.flagHandlerflagHandler = FlagHandler.addFields(schema, name, flagDefs)
115 
116  self.centriodExtractorcentriodExtractor = SafeCentroidExtractor(schema, name)
117 
118  def measure(self, measRecord, exposure):
119  """Run the Naive trailed source measurement algorithm.
120 
121  Parameters
122  ----------
123  measRecord : `lsst.afw.table.SourceRecord`
124  Record describing the object being measured.
125  exposure : `lsst.afw.image.Exposure`
126  Pixel data to be measured.
127 
128  See also
129  --------
130  lsst.meas.base.SingleFramePlugin.measure
131  """
132 
133  # Get the SdssShape centroid or fall back to slot
134  xc = measRecord.get("base_SdssShape_x")
135  yc = measRecord.get("base_SdssShape_y")
136  if not np.isfinite(xc) or not np.isfinite(yc):
137  xc, yc = self.centriodExtractorcentriodExtractor(measRecord, self.flagHandlerflagHandler)
138 
139  ra, dec = self.computeRaDeccomputeRaDec(exposure, xc, yc)
140 
141  Ixx, Iyy, Ixy = measRecord.getShape().getParameterVector()
142  xmy = Ixx - Iyy
143  xpy = Ixx + Iyy
144  xmy2 = xmy*xmy
145  xy2 = Ixy*Ixy
146  a2 = 0.5 * (xpy + np.sqrt(xmy2 + 4.0*xy2))
147  sigma = exposure.getPsf().getSigma()
148 
149  length, results = self.findLengthfindLength(a2, sigma*sigma)
150  if not results.converged:
151  lsst.log.info(results.flag)
152  raise MeasurementError(self.NO_CONVERGENO_CONVERGE.doc, self.NO_CONVERGENO_CONVERGE.number)
153 
154  theta = 0.5 * np.arctan2(2.0 * Ixy, xmy)
155  a = length/2.0
156  dydt = a*np.cos(theta)
157  dxdt = a*np.sin(theta)
158  x0 = xc - dydt
159  y0 = yc - dxdt
160  x1 = xc + dydt
161  y1 = yc + dxdt
162 
163  # For now, use the shape flux.
164  flux = measRecord.get("base_SdssShape_instFlux")
165 
166  # Fall back to aperture flux
167  if not np.isfinite(flux):
168  if np.isfinite(measRecord.getApInstFlux()):
169  flux = measRecord.getApInstFlux()
170  else:
171  raise MeasurementError(self.NO_FLUXNO_FLUX.doc, self.NO_FLUXNO_FLUX.number)
172 
173  # Propagate errors from second moments
174  xcErr2, ycErr2 = np.diag(measRecord.getCentroidErr())
175  IxxErr2, IyyErr2, IxyErr2 = np.diag(measRecord.getShapeErr())
176  desc = np.sqrt(xmy2 + 4.0*xy2) # Descriminant^1/2 of EV equation
177  denom = 2*np.sqrt(2.0*(Ixx + np.sqrt(4.0*xy2 + xmy2 + Iyy))) # Denominator for dadIxx and dadIyy
178  dadIxx = (1.0 + (xmy/desc)) / denom
179  dadIyy = (1.0 - (xmy/desc)) / denom
180  dadIxy = (4.0*Ixy) / (desc * denom)
181  aErr2 = IxxErr2*dadIxx*dadIxx + IyyErr2*dadIyy*dadIyy + IxyErr2*dadIxy*dadIxy
182  thetaErr2 = ((IxxErr2 + IyyErr2)*xy2 + xmy2*IxyErr2) / (desc*desc*desc*desc)
183 
184  dxda = np.cos(theta)
185  dyda = np.sin(theta)
186  xErr2 = aErr2*dxda*dxda + thetaErr2*dxdt*dxdt
187  yErr2 = aErr2*dyda*dyda + thetaErr2*dydt*dydt
188  x0Err = np.sqrt(xErr2 + xcErr2) # Same for x1
189  y0Err = np.sqrt(yErr2 + ycErr2) # Same for y1
190 
191  # Set flags
192  measRecord.set(self.keyRakeyRa, ra)
193  measRecord.set(self.keyDeckeyDec, dec)
194  measRecord.set(self.keyX0keyX0, x0)
195  measRecord.set(self.keyY0keyY0, y0)
196  measRecord.set(self.keyX1keyX1, x1)
197  measRecord.set(self.keyY1keyY1, y1)
198  measRecord.set(self.keyFluxkeyFlux, flux)
199  measRecord.set(self.keyLkeyL, length)
200  measRecord.set(self.keyAnglekeyAngle, theta)
201  measRecord.set(self.keyX0ErrkeyX0Err, x0Err)
202  measRecord.set(self.keyY0ErrkeyY0Err, y0Err)
203  measRecord.set(self.keyX1ErrkeyX1Err, x0Err)
204  measRecord.set(self.keyY1ErrkeyY1Err, y0Err)
205 
206  def fail(self, measRecord, error=None):
207  """Record failure
208 
209  See also
210  --------
211  lsst.meas.base.SingleFramePlugin.fail
212  """
213  if error is None:
214  self.flagHandlerflagHandler.handleFailure(measRecord)
215  else:
216  self.flagHandlerflagHandler.handleFailure(measRecord, error.cpp)
217 
218  def _computeSecondMomentDiff(self, z, c):
219  """Compute difference of the numerical and analytic second moments.
220 
221  Parameters
222  ----------
223  z : `float`
224  Proportional to the length of the trail. (see notes)
225  c : `float`
226  Constant (see notes)
227 
228  Returns
229  -------
230  diff : `float`
231  Difference in numerical and analytic second moments.
232 
233  Notes
234  -----
235  This is a simplified expression for the difference between the stack
236  computed adaptive second-moment and the analytic solution. The variable
237  z is proportional to the length such that L = 2*z*sqrt(2*(Ixx+Iyy)),
238  and c is a constant (c = 4*Ixx/((Ixx+Iyy)*sqrt(pi))). Both have been
239  defined to avoid unnecessary floating-point operations in the root
240  finder.
241  """
242 
243  diff = erf(z) - c*z*np.exp(-z*z)
244  return diff
245 
246  def findLength(self, Ixx, Iyy):
247  """Find the length of a trail, given adaptive second-moments.
248 
249  Uses a root finder to compute the length of a trail corresponding to
250  the adaptive second-moments computed by previous measurements
251  (ie. SdssShape).
252 
253  Parameters
254  ----------
255  Ixx : `float`
256  Adaptive second-moment along x-axis.
257  Iyy : `float`
258  Adaptive second-moment along y-axis.
259 
260  Returns
261  -------
262  length : `float`
263  Length of the trail.
264  results : `scipy.optimize.RootResults`
265  Contains messages about convergence from the root finder.
266  """
267 
268  xpy = Ixx + Iyy
269  c = 4.0*Ixx/(xpy*np.sqrt(np.pi))
270 
271  # Given a 'c' in (c_min, c_max], the root is contained in (0,1].
272  # c_min is given by the case: Ixx == Iyy, ie. a point source.
273  # c_max is given by the limit Ixx >> Iyy.
274  # Emperically, 0.001 is a suitable lower bound, assuming Ixx > Iyy.
275  z, results = sciOpt.brentq(lambda z: self._computeSecondMomentDiff_computeSecondMomentDiff(z, c),
276  0.001, 1.0, full_output=True)
277 
278  length = 2.0*z*np.sqrt(2.0*xpy)
279  return length, results
280 
281  def computeRaDec(self, exposure, x, y):
282  """Convert pixel coordinates to RA and Dec.
283 
284  Parameters
285  ----------
286  exposure : `lsst.afw.image.ExposureF`
287  Exposure object containing the WCS.
288  x : `float`
289  x coordinate of the trail centroid
290  y : `float`
291  y coodinate of the trail centroid
292 
293  Returns
294  -------
295  ra : `float`
296  Right ascension.
297  dec : `float`
298  Declination.
299  """
300 
301  wcs = exposure.getWcs()
302  center = wcs.pixelToSky(Point2D(x, y))
303  ra = center.getRa().asDegrees()
304  dec = center.getDec().asDegrees()
305  return ra, dec