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

71 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-06-02 04:06 -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# 

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 

36from .utils import getMeasurementCutout 

37 

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

39 

40 

41class SingleFrameVeresTrailConfig(SingleFramePluginConfig): 

42 """Config class for SingleFrameVeresTrailPlugin 

43 """ 

44 

45 optimizerMethod = Field( 

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

47 dtype=str, 

48 default="L-BFGS-B" 

49 ) 

50 

51 

52@register("ext_trailedSources_Veres") 

53class SingleFrameVeresTrailPlugin(SingleFramePlugin): 

54 """Veres trailed source characterization plugin. 

55 

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

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

58 

59 Parameters 

60 ---------- 

61 config: `SingleFrameNaiveTrailConfig` 

62 Plugin configuration. 

63 name: `str` 

64 Plugin name. 

65 schema: `lsst.afw.table.Schema` 

66 Schema for the output catalog. 

67 metadata: `lsst.daf.base.PropertySet` 

68 Metadata to be attached to output catalog. 

69 

70 Notes 

71 ----- 

72 This plugin is designed to refine the measurements of trail length, 

73 angle, and end points from `NaivePlugin`, and of flux and centroid from 

74 previous measurement algorithms. Vereš et al. 2012 [1]_ derive a model for 

75 the flux in a given image pixel by convolving an axisymmetric Gaussian with 

76 a line. The model is parameterized by the total flux, trail length, angle 

77 from the x-axis, and the centroid. The best estimates are computed using a 

78 chi-squared minimization. 

79 

80 References 

81 ---------- 

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

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

84 

85 See also 

86 -------- 

87 lsst.meas.base.SingleFramePlugin 

88 """ 

89 

90 ConfigClass = SingleFrameVeresTrailConfig 

91 

92 @classmethod 

93 def getExecutionOrder(cls): 

94 # Needs centroids, shape, flux, and NaivePlugin measurements. 

95 # Make sure this always runs after NaivePlugin. 

96 return SingleFrameNaiveTrailPlugin.getExecutionOrder() + 0.1 

97 

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

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

100 

101 self.keyXC = schema.addField( 

102 name + "_centroid_x", type="D", doc="Trail centroid X coordinate.", units="pixel") 

103 self.keyYC = schema.addField( 

104 name + "_centroid_y", type="D", doc="Trail centroid Y coordinate.", units="pixel") 

105 self.keyX0 = schema.addField(name + "_x0", type="D", doc="Trail head X coordinate.", units="pixel") 

106 self.keyY0 = schema.addField(name + "_y0", type="D", doc="Trail head Y coordinate.", units="pixel") 

107 self.keyX1 = schema.addField(name + "_x1", type="D", doc="Trail tail X coordinate.", units="pixel") 

108 self.keyY1 = schema.addField(name + "_y1", type="D", doc="Trail tail Y coordinate.", units="pixel") 

109 self.keyLength = schema.addField(name + "_length", type="D", doc="Length of trail.", units="pixel") 

110 self.keyTheta = schema.addField(name + "_angle", type="D", doc="Angle of trail from +x-axis.") 

111 self.keyFlux = schema.addField(name + "_flux", type="D", doc="Trailed source flux.", units="count") 

112 self.keyRChiSq = schema.addField(name + "_rChiSq", type="D", doc="Reduced chi-squared of fit") 

113 

114 flagDefs = FlagDefinitionList() 

115 flagDefs.addFailureFlag("No trailed-sources measured") 

116 self.NON_CONVERGE = flagDefs.add("flag_nonConvergence", "Optimizer did not converge") 

117 self.NO_NAIVE = flagDefs.add("flag_noNaive", "Naive measurement contains NaNs") 

118 self.flagHandler = FlagHandler.addFields(schema, name, flagDefs) 

119 

120 self.centroidExtractor = SafeCentroidExtractor(schema, name) 

121 

122 def measure(self, measRecord, exposure): 

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

124 

125 Parameters 

126 ---------- 

127 measRecord : `lsst.afw.table.SourceRecord` 

128 Record describing the object being measured. 

129 exposure : `lsst.afw.image.Exposure` 

130 Pixel data to be measured. 

131 

132 See also 

133 -------- 

134 lsst.meas.base.SingleFramePlugin.measure 

135 """ 

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

137 

138 # Look at measRecord for Naive measurements 

139 # ASSUMES NAIVE ALREADY RAN 

140 flux = measRecord.get("ext_trailedSources_Naive_flux") 

141 length = measRecord.get("ext_trailedSources_Naive_length") 

142 theta = measRecord.get("ext_trailedSources_Naive_angle") 

143 if not np.isfinite(flux) or not np.isfinite(length) or not np.isfinite(theta): 

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

145 

146 # Get exposure cutout 

147 # sigma = exposure.getPsf().getSigma() 

148 # cutout = getMeasurementCutout(exposure, xc, yc, length, sigma) 

149 cutout = getMeasurementCutout(measRecord, exposure) 

150 

151 # Make VeresModel 

152 model = VeresModel(cutout) 

153 

154 # Do optimization with scipy 

155 params = np.array([xc, yc, flux, length, theta]) 

156 results = sciOpt.minimize( 

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

158 

159 # Check if optimizer converged 

160 if not results.success: 

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

162 

163 # Calculate end points and reduced chi-squared 

164 xc_fit, yc_fit, flux_fit, length_fit, theta_fit = results.x 

165 a = length_fit/2 

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

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

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

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

170 rChiSq = results.fun / (cutout.image.array.size - 6) 

171 

172 # Set keys 

173 measRecord.set(self.keyXC, xc_fit) 

174 measRecord.set(self.keyYC, yc_fit) 

175 measRecord.set(self.keyX0, x0_fit) 

176 measRecord.set(self.keyY0, y0_fit) 

177 measRecord.set(self.keyX1, x1_fit) 

178 measRecord.set(self.keyY1, y1_fit) 

179 measRecord.set(self.keyFlux, flux_fit) 

180 measRecord.set(self.keyLength, length_fit) 

181 measRecord.set(self.keyTheta, theta_fit) 

182 measRecord.set(self.keyRChiSq, rChiSq) 

183 

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

185 """Record failure 

186 

187 See also 

188 -------- 

189 lsst.meas.base.SingleFramePlugin.fail 

190 """ 

191 if error is None: 

192 self.flagHandler.handleFailure(measRecord) 

193 else: 

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