LSST Applications  21.0.0-147-g0e635eb1+1acddb5be5,22.0.0+052faf71bd,22.0.0+1ea9a8b2b2,22.0.0+6312710a6c,22.0.0+729191ecac,22.0.0+7589c3a021,22.0.0+9f079a9461,22.0.1-1-g7d6de66+b8044ec9de,22.0.1-1-g87000a6+536b1ee016,22.0.1-1-g8e32f31+6312710a6c,22.0.1-10-gd060f87+016f7cdc03,22.0.1-12-g9c3108e+df145f6f68,22.0.1-16-g314fa6d+c825727ab8,22.0.1-19-g93a5c75+d23f2fb6d8,22.0.1-19-gb93eaa13+aab3ef7709,22.0.1-2-g8ef0a89+b8044ec9de,22.0.1-2-g92698f7+9f079a9461,22.0.1-2-ga9b0f51+052faf71bd,22.0.1-2-gac51dbf+052faf71bd,22.0.1-2-gb66926d+6312710a6c,22.0.1-2-gcb770ba+09e3807989,22.0.1-20-g32debb5+b8044ec9de,22.0.1-23-gc2439a9a+fb0756638e,22.0.1-3-g496fd5d+09117f784f,22.0.1-3-g59f966b+1e6ba2c031,22.0.1-3-g849a1b8+f8b568069f,22.0.1-3-gaaec9c0+c5c846a8b1,22.0.1-32-g5ddfab5d3+60ce4897b0,22.0.1-4-g037fbe1+64e601228d,22.0.1-4-g8623105+b8044ec9de,22.0.1-5-g096abc9+d18c45d440,22.0.1-5-g15c806e+57f5c03693,22.0.1-7-gba73697+57f5c03693,master-g6e05de7fdc+c1283a92b8,master-g72cdda8301+729191ecac,w.2021.39
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.meas.base.pluginRegistry import register
30 from lsst.meas.base import SingleFramePlugin, SingleFramePluginConfig
31 from lsst.meas.base import FlagHandler, FlagDefinitionList, SafeCentroidExtractor
32 from lsst.meas.base import MeasurementError
33 
34 __all__ = ("SingleFrameNaiveTrailConfig", "SingleFrameNaiveTrailPlugin")
35 
36 
38  """Config class for SingleFrameNaiveTrailPlugin.
39  """
40  pass
41 
42 
43 @register("ext_trailedSources_Naive")
45  """Naive trailed source measurement plugin
46 
47  Measures the length, angle from +x-axis, and end points of an extended
48  source using the second moments.
49 
50  Parameters
51  ----------
52  config: `SingleFrameNaiveTrailConfig`
53  Plugin configuration.
54  name: `str`
55  Plugin name.
56  schema: `lsst.afw.table.Schema`
57  Schema for the output catalog.
58  metadata: `lsst.daf.base.PropertySet`
59  Metadata to be attached to output catalog.
60 
61  Notes
62  -----
63  This measurement plugin aims to utilize the already measured adaptive
64  second moments to naively estimate the length and angle, and thus
65  end-points, of a fast-moving, trailed source. The length is solved for via
66  finding the root of the difference between the numerical (stack computed)
67  and the analytic adaptive second moments. The angle, theta, from the x-axis
68  is also computed via adaptive moments: theta = arctan(2*Ixy/(Ixx - Iyy))/2.
69  The end points of the trail are then given by (xc +/- (L/2)*cos(theta),
70  yc +/- (L/2)*sin(theta)), with xc and yc being the centroid coordinates.
71 
72  See also
73  --------
74  lsst.meas.base.SingleFramePlugin
75  """
76 
77  ConfigClass = SingleFrameNaiveTrailConfig
78 
79  @classmethod
81  # Needs centroids, shape, and flux measurements.
82  # VeresPlugin is run after, which requires image data.
83  return cls.APCORR_ORDERAPCORR_ORDER + 0.1
84 
85  def __init__(self, config, name, schema, metadata):
86  super().__init__(config, name, schema, metadata)
87 
88  # Measurement Keys
89  self.keyX0keyX0 = schema.addField(name + "_x0", type="D", doc="Trail head X coordinate.", units="pixel")
90  self.keyY0keyY0 = schema.addField(name + "_y0", type="D", doc="Trail head Y coordinate.", units="pixel")
91  self.keyX1keyX1 = schema.addField(name + "_x1", type="D", doc="Trail tail X coordinate.", units="pixel")
92  self.keyY1keyY1 = schema.addField(name + "_y1", type="D", doc="Trail tail Y coordinate.", units="pixel")
93  self.keyFluxkeyFlux = schema.addField(name + "_flux", type="D", doc="Trailed source flux.", units="count")
94  self.keyLkeyL = schema.addField(name + "_length", type="D", doc="Trail length.", units="pixel")
95  self.keyAnglekeyAngle = schema.addField(name + "_angle", type="D", doc="Angle measured from +x-axis.")
96 
97  # Measurement Error Keys
98  self.keyX0ErrkeyX0Err = schema.addField(name + "_x0Err", type="D",
99  doc="Trail head X coordinate error.", units="pixel")
100  self.keyY0ErrkeyY0Err = schema.addField(name + "_y0Err", type="D",
101  doc="Trail head Y coordinate error.", units="pixel")
102  self.keyX1ErrkeyX1Err = schema.addField(name + "_x1Err", type="D",
103  doc="Trail tail X coordinate error.", units="pixel")
104  self.keyY1ErrkeyY1Err = schema.addField(name + "_y1Err", type="D",
105  doc="Trail tail Y coordinate error.", units="pixel")
106 
107  flagDefs = FlagDefinitionList()
108  flagDefs.addFailureFlag("No trailed-source measured")
109  self.NO_FLUXNO_FLUX = flagDefs.add("flag_noFlux", "No suitable prior flux measurement")
110  self.NO_CONVERGENO_CONVERGE = flagDefs.add("flag_noConverge", "The root finder did not converge")
111  self.flagHandlerflagHandler = FlagHandler.addFields(schema, name, flagDefs)
112 
113  self.centriodExtractorcentriodExtractor = SafeCentroidExtractor(schema, name)
114 
115  def measure(self, measRecord, exposure):
116  """Run the Naive trailed source measurement algorithm.
117 
118  Parameters
119  ----------
120  measRecord : `lsst.afw.table.SourceRecord`
121  Record describing the object being measured.
122  exposure : `lsst.afw.image.Exposure`
123  Pixel data to be measured.
124 
125  See also
126  --------
127  lsst.meas.base.SingleFramePlugin.measure
128  """
129  xc, yc = self.centriodExtractorcentriodExtractor(measRecord, self.flagHandlerflagHandler)
130  Ixx, Iyy, Ixy = measRecord.getShape().getParameterVector()
131  xmy = Ixx - Iyy
132  xpy = Ixx + Iyy
133  xmy2 = xmy*xmy
134  xy2 = Ixy*Ixy
135  a2 = 0.5 * (xpy + np.sqrt(xmy2 + 4.0*xy2))
136  sigma = exposure.getPsf().getSigma()
137 
138  length, results = self.findLengthfindLength(a2, sigma*sigma)
139  if not results.converged:
140  lsst.log.info(results.flag)
141  raise MeasurementError(self.NO_CONVERGENO_CONVERGE.doc, self.NO_CONVERGENO_CONVERGE.number)
142 
143  theta = 0.5 * np.arctan2(2.0 * Ixy, xmy)
144  a = length/2.0
145  dydt = a*np.cos(theta)
146  dxdt = a*np.sin(theta)
147  x0 = xc - dydt
148  y0 = yc - dxdt
149  x1 = xc + dydt
150  y1 = yc + dxdt
151 
152  # For now, use the shape flux.
153  flux = measRecord.get("base_SdssShape_instFlux")
154 
155  # Fall back to aperture flux
156  if not np.isfinite(flux):
157  if np.isfinite(measRecord.getApInstFlux()):
158  flux = measRecord.getApInstFlux()
159  else:
160  raise MeasurementError(self.NO_FLUXNO_FLUX.doc, self.NO_FLUXNO_FLUX.number)
161 
162  # Propagate errors from second moments
163  xcErr2, ycErr2 = np.diag(measRecord.getCentroidErr())
164  IxxErr2, IyyErr2, IxyErr2 = np.diag(measRecord.getShapeErr())
165  desc = np.sqrt(xmy2 + 4.0*xy2) # Descriminant^1/2 of EV equation
166  denom = 2*np.sqrt(2.0*(Ixx + np.sqrt(4.0*xy2 + xmy2 + Iyy))) # Denominator for dadIxx and dadIyy
167  dadIxx = (1.0 + (xmy/desc)) / denom
168  dadIyy = (1.0 - (xmy/desc)) / denom
169  dadIxy = (4.0*Ixy) / (desc * denom)
170  aErr2 = IxxErr2*dadIxx*dadIxx + IyyErr2*dadIyy*dadIyy + IxyErr2*dadIxy*dadIxy
171  thetaErr2 = ((IxxErr2 + IyyErr2)*xy2 + xmy2*IxyErr2) / (desc*desc*desc*desc)
172 
173  dxda = np.cos(theta)
174  dyda = np.sin(theta)
175  xErr2 = aErr2*dxda*dxda + thetaErr2*dxdt*dxdt
176  yErr2 = aErr2*dyda*dyda + thetaErr2*dydt*dydt
177  x0Err = np.sqrt(xErr2 + xcErr2) # Same for x1
178  y0Err = np.sqrt(yErr2 + ycErr2) # Same for y1
179 
180  # Set flags
181  measRecord.set(self.keyX0keyX0, x0)
182  measRecord.set(self.keyY0keyY0, y0)
183  measRecord.set(self.keyX1keyX1, x1)
184  measRecord.set(self.keyY1keyY1, y1)
185  measRecord.set(self.keyFluxkeyFlux, flux)
186  measRecord.set(self.keyLkeyL, length)
187  measRecord.set(self.keyAnglekeyAngle, theta)
188  measRecord.set(self.keyX0ErrkeyX0Err, x0Err)
189  measRecord.set(self.keyY0ErrkeyY0Err, y0Err)
190  measRecord.set(self.keyX1ErrkeyX1Err, x0Err)
191  measRecord.set(self.keyY1ErrkeyY1Err, y0Err)
192 
193  def fail(self, measRecord, error=None):
194  """Record failure
195 
196  See also
197  --------
198  lsst.meas.base.SingleFramePlugin.fail
199  """
200  if error is None:
201  self.flagHandlerflagHandler.handleFailure(measRecord)
202  else:
203  self.flagHandlerflagHandler.handleFailure(measRecord, error.cpp)
204 
205  def _computeSecondMomentDiff(self, z, c):
206  """Compute difference of the numerical and analytic second moments.
207 
208  Parameters
209  ----------
210  z : `float`
211  Proportional to the length of the trail. (see notes)
212  c : `float`
213  Constant (see notes)
214 
215  Returns
216  -------
217  diff : `float`
218  Difference in numerical and analytic second moments.
219 
220  Notes
221  -----
222  This is a simplified expression for the difference between the stack
223  computed adaptive second-moment and the analytic solution. The variable
224  z is proportional to the length such that L = 2*z*sqrt(2*(Ixx+Iyy)),
225  and c is a constant (c = 4*Ixx/((Ixx+Iyy)*sqrt(pi))). Both have been
226  defined to avoid unnecessary floating-point operations in the root
227  finder.
228  """
229 
230  diff = erf(z) - c*z*np.exp(-z*z)
231  return diff
232 
233  def findLength(self, Ixx, Iyy):
234  """Find the length of a trail, given adaptive second-moments.
235 
236  Uses a root finder to compute the length of a trail corresponding to
237  the adaptive second-moments computed by previous measurements
238  (ie. SdssShape).
239 
240  Parameters
241  ----------
242  Ixx : `float`
243  Adaptive second-moment along x-axis.
244  Iyy : `float`
245  Adaptive second-moment along y-axis.
246 
247  Returns
248  -------
249  length : `float`
250  Length of the trail.
251  results : `scipy.optimize.RootResults`
252  Contains messages about convergence from the root finder.
253  """
254 
255  xpy = Ixx + Iyy
256  c = 4.0*Ixx/(xpy*np.sqrt(np.pi))
257 
258  # Given a 'c' in (c_min, c_max], the root is contained in (0,1].
259  # c_min is given by the case: Ixx == Iyy, ie. a point source.
260  # c_max is given by the limit Ixx >> Iyy.
261  # Emperically, 0.001 is a suitable lower bound, assuming Ixx > Iyy.
262  z, results = sciOpt.brentq(lambda z: self._computeSecondMomentDiff_computeSecondMomentDiff(z, c),
263  0.001, 1.0, full_output=True)
264 
265  length = 2.0*z*np.sqrt(2.0*xpy)
266  return length, results
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 ...
Definition: Log.h:717