Coverage for python/lsst/meas/extensions/trailedSources/VeresPlugin.py: 29%

69 statements  

« prev     ^ index     » next       coverage.py v7.1.0, created at 2023-02-05 18:15 -0800

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 

24import numpy as np 

25import scipy.optimize as sciOpt 

26 

27from lsst.pex.config import Field 

28 

29from lsst.meas.base.pluginRegistry import register 

30from lsst.meas.base import SingleFramePlugin, SingleFramePluginConfig 

31from lsst.meas.base import FlagHandler, FlagDefinitionList, SafeCentroidExtractor 

32from lsst.meas.base import MeasurementError 

33 

34from ._trailedSources import VeresModel 

35from .NaivePlugin import SingleFrameNaiveTrailPlugin 

36 

37__all__ = ("SingleFrameVeresTrailConfig", "SingleFrameVeresTrailPlugin") 

38 

39 

40class SingleFrameVeresTrailConfig(SingleFramePluginConfig): 

41 """Config class for SingleFrameVeresTrailPlugin 

42 """ 

43 

44 optimizerMethod = Field( 

45 doc="Optimizer method for scipy.optimize.minimize", 

46 dtype=str, 

47 default="L-BFGS-B" 

48 ) 

49 

50 

51@register("ext_trailedSources_Veres") 

52class SingleFrameVeresTrailPlugin(SingleFramePlugin): 

53 """Veres trailed source characterization plugin. 

54 

55 Measures the length, angle, flux, centroid, and end points of a trailed 

56 source using the Veres et al. 2012 model [1]_. 

57 

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. 

68 

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. 

78 

79 References 

80 ---------- 

81 .. [1] Vereš, P., et al. "Improved Asteroid Astrometry and Photometry with 

82 Trail Fitting" PASP, vol. 124, 2012. 

83 

84 See also 

85 -------- 

86 lsst.meas.base.SingleFramePlugin 

87 """ 

88 

89 ConfigClass = SingleFrameVeresTrailConfig 

90 

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 

96 

97 def __init__(self, config, name, schema, metadata): 

98 super().__init__(config, name, schema, metadata) 

99 

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.keyL = 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") 

112 

113 flagDefs = FlagDefinitionList() 

114 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) 

118 

119 self.centroidExtractor = SafeCentroidExtractor(schema, name) 

120 

121 def measure(self, measRecord, exposure): 

122 """Run the Veres trailed source measurement plugin. 

123 

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. 

130 

131 See also 

132 -------- 

133 lsst.meas.base.SingleFramePlugin.measure 

134 """ 

135 xc, yc = self.centroidExtractor(measRecord, self.flagHandler) 

136 

137 # Look at measRecord for Naive measurements 

138 # ASSUMES NAIVE ALREADY RAN 

139 F = measRecord.get("ext_trailedSources_Naive_flux") 

140 L = measRecord.get("ext_trailedSources_Naive_length") 

141 theta = measRecord.get("ext_trailedSources_Naive_angle") 

142 if not np.isfinite(F) or not np.isfinite(L) or not np.isfinite(theta): 

143 raise MeasurementError(self.NO_NAIVE.doc, self.NO_NAIVE.number) 

144 

145 # Make VeresModel 

146 model = VeresModel(exposure) 

147 

148 # Do optimization with scipy 

149 params = np.array([xc, yc, F, L, theta]) 

150 results = sciOpt.minimize( 

151 model, params, method=self.config.optimizerMethod, jac=model.gradient) 

152 

153 # Check if optimizer converged 

154 if not results.success: 

155 raise MeasurementError(self.NON_CONVERGE.doc, self.NON_CONVERGE.number) 

156 

157 # Calculate end points and reduced chi-squared 

158 xc_fit, yc_fit, F_fit, L_fit, theta_fit = results.x 

159 a = L_fit/2 

160 x0_fit = xc_fit - a * np.cos(theta_fit) 

161 y0_fit = yc_fit - a * np.sin(theta_fit) 

162 x1_fit = xc_fit + a * np.cos(theta_fit) 

163 y1_fit = yc_fit + a * np.sin(theta_fit) 

164 rChiSq = results.fun / (exposure.image.array.size - 6) 

165 

166 # Set keys 

167 measRecord.set(self.keyXC, xc_fit) 

168 measRecord.set(self.keyYC, yc_fit) 

169 measRecord.set(self.keyX0, x0_fit) 

170 measRecord.set(self.keyY0, y0_fit) 

171 measRecord.set(self.keyX1, x1_fit) 

172 measRecord.set(self.keyY1, y1_fit) 

173 measRecord.set(self.keyFlux, F_fit) 

174 measRecord.set(self.keyL, L_fit) 

175 measRecord.set(self.keyTheta, theta_fit) 

176 measRecord.set(self.keyRChiSq, rChiSq) 

177 

178 def fail(self, measRecord, error=None): 

179 """Record failure 

180 

181 See also 

182 -------- 

183 lsst.meas.base.SingleFramePlugin.fail 

184 """ 

185 if error is None: 

186 self.flagHandler.handleFailure(measRecord) 

187 else: 

188 self.flagHandler.handleFailure(measRecord, error.cpp)