Coverage for python/lsst/meas/extensions/trailedSources/VeresPlugin.py: 27%
74 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-03 03:14 -0700
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-03 03:14 -0700
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#
24import numpy as np
25import scipy.optimize as sciOpt
27from lsst.pex.config import Field
29from lsst.meas.base.pluginRegistry import register
30from lsst.meas.base import SingleFramePlugin, SingleFramePluginConfig
31from lsst.meas.base import FlagHandler, FlagDefinitionList, SafeCentroidExtractor
33from ._trailedSources import VeresModel
34from .NaivePlugin import SingleFrameNaiveTrailPlugin
35from .utils import getMeasurementCutout
37__all__ = ("SingleFrameVeresTrailConfig", "SingleFrameVeresTrailPlugin")
40class SingleFrameVeresTrailConfig(SingleFramePluginConfig):
41 """Config class for SingleFrameVeresTrailPlugin
42 """
44 optimizerMethod = Field(
45 doc="Optimizer method for scipy.optimize.minimize",
46 dtype=str,
47 default="L-BFGS-B"
48 )
51@register("ext_trailedSources_Veres")
52class SingleFrameVeresTrailPlugin(SingleFramePlugin):
53 """Veres trailed source characterization plugin.
55 Measures the length, angle, flux, centroid, and end points of a trailed
56 source using the Veres et al. 2012 model [1]_.
58 Parameters
59 ----------
60 config: `SingleFrameNaiveTrailConfig`
61 Plugin configuration.
62 name: `str`
63 Plugin name.
64 schema: `lsst.afw.table.Schema`
65 Schema for the output catalog.
66 metadata: `lsst.daf.base.PropertySet`
67 Metadata to be attached to output catalog.
69 Notes
70 -----
71 This plugin is designed to refine the measurements of trail length,
72 angle, and end points from `NaivePlugin`, and of flux and centroid from
73 previous measurement algorithms. Vereš et al. 2012 [1]_ derive a model for
74 the flux in a given image pixel by convolving an axisymmetric Gaussian with
75 a line. The model is parameterized by the total flux, trail length, angle
76 from the x-axis, and the centroid. The best estimates are computed using a
77 chi-squared minimization.
79 References
80 ----------
81 .. [1] Vereš, P., et al. "Improved Asteroid Astrometry and Photometry with
82 Trail Fitting" PASP, vol. 124, 2012.
84 See also
85 --------
86 lsst.meas.base.SingleFramePlugin
87 """
89 ConfigClass = SingleFrameVeresTrailConfig
91 @classmethod
92 def getExecutionOrder(cls):
93 # Needs centroids, shape, flux, and NaivePlugin measurements.
94 # Make sure this always runs after NaivePlugin.
95 return SingleFrameNaiveTrailPlugin.getExecutionOrder() + 0.1
97 def __init__(self, config, name, schema, metadata, logName=None):
98 super().__init__(config, name, schema, metadata, logName=logName)
100 self.keyXC = schema.addField(
101 name + "_centroid_x", type="D", doc="Trail centroid X coordinate.", units="pixel")
102 self.keyYC = schema.addField(
103 name + "_centroid_y", type="D", doc="Trail centroid Y coordinate.", units="pixel")
104 self.keyX0 = schema.addField(name + "_x0", type="D", doc="Trail head X coordinate.", units="pixel")
105 self.keyY0 = schema.addField(name + "_y0", type="D", doc="Trail head Y coordinate.", units="pixel")
106 self.keyX1 = schema.addField(name + "_x1", type="D", doc="Trail tail X coordinate.", units="pixel")
107 self.keyY1 = schema.addField(name + "_y1", type="D", doc="Trail tail Y coordinate.", units="pixel")
108 self.keyLength = schema.addField(name + "_length", type="D", doc="Length of trail.", units="pixel")
109 self.keyTheta = schema.addField(name + "_angle", type="D", doc="Angle of trail from +x-axis.")
110 self.keyFlux = schema.addField(name + "_flux", type="D", doc="Trailed source flux.", units="count")
111 self.keyRChiSq = schema.addField(name + "_rChiSq", type="D", doc="Reduced chi-squared of fit")
113 flagDefs = FlagDefinitionList()
114 self.FAILURE = flagDefs.addFailureFlag("No trailed-sources measured")
115 self.NON_CONVERGE = flagDefs.add("flag_nonConvergence", "Optimizer did not converge")
116 self.NO_NAIVE = flagDefs.add("flag_noNaive", "Naive measurement contains NaNs")
117 self.flagHandler = FlagHandler.addFields(schema, name, flagDefs)
119 self.centroidExtractor = SafeCentroidExtractor(schema, name)
121 def measure(self, measRecord, exposure):
122 """Run the Veres trailed source measurement plugin.
124 Parameters
125 ----------
126 measRecord : `lsst.afw.table.SourceRecord`
127 Record describing the object being measured.
128 exposure : `lsst.afw.image.Exposure`
129 Pixel data to be measured.
131 See also
132 --------
133 lsst.meas.base.SingleFramePlugin.measure
134 """
135 xc, yc = self.centroidExtractor(measRecord, self.flagHandler)
137 # Look at measRecord for Naive measurements
138 # ASSUMES NAIVE ALREADY RAN
139 flux = measRecord.get("ext_trailedSources_Naive_flux")
140 length = measRecord.get("ext_trailedSources_Naive_length")
141 theta = measRecord.get("ext_trailedSources_Naive_angle")
142 if not np.isfinite(flux) or not np.isfinite(length) or not np.isfinite(theta):
143 self.flagHandler.setValue(measRecord, self.NO_NAIVE.number)
144 self.flagHandler.setValue(measRecord, self.FAILURE.number)
145 return
147 # Get exposure cutout
148 # sigma = exposure.getPsf().getSigma()
149 # cutout = getMeasurementCutout(exposure, xc, yc, length, sigma)
150 cutout = getMeasurementCutout(measRecord, exposure)
152 # Make VeresModel
153 model = VeresModel(cutout)
155 # Do optimization with scipy
156 params = np.array([xc, yc, flux, length, theta])
157 results = sciOpt.minimize(
158 model, params, method=self.config.optimizerMethod, jac=model.gradient)
160 # Check if optimizer converged
161 if not results.success:
162 self.flagHandler.setValue(measRecord, self.NON_CONVERGE.number)
163 self.flagHandler.setValue(measRecord, self.FAILURE.number)
164 return
166 # Calculate end points and reduced chi-squared
167 xc_fit, yc_fit, flux_fit, length_fit, theta_fit = results.x
168 a = length_fit/2
169 x0_fit = xc_fit - a * np.cos(theta_fit)
170 y0_fit = yc_fit - a * np.sin(theta_fit)
171 x1_fit = xc_fit + a * np.cos(theta_fit)
172 y1_fit = yc_fit + a * np.sin(theta_fit)
173 rChiSq = results.fun / (cutout.image.array.size - 6)
175 # Set keys
176 measRecord.set(self.keyXC, xc_fit)
177 measRecord.set(self.keyYC, yc_fit)
178 measRecord.set(self.keyX0, x0_fit)
179 measRecord.set(self.keyY0, y0_fit)
180 measRecord.set(self.keyX1, x1_fit)
181 measRecord.set(self.keyY1, y1_fit)
182 measRecord.set(self.keyFlux, flux_fit)
183 measRecord.set(self.keyLength, length_fit)
184 measRecord.set(self.keyTheta, theta_fit)
185 measRecord.set(self.keyRChiSq, rChiSq)
187 def fail(self, measRecord, error=None):
188 """Record failure
190 See also
191 --------
192 lsst.meas.base.SingleFramePlugin.fail
193 """
194 if error is None:
195 self.flagHandler.handleFailure(measRecord)
196 else:
197 self.flagHandler.handleFailure(measRecord, error.cpp)