Coverage for python/lsst/meas/extensions/trailedSources/NaivePlugin.py: 20%

101 statements  

« prev     ^ index     » next       coverage.py v7.2.1, created at 2023-03-12 10:09 +0000

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 

26from scipy.special import erf 

27 

28import lsst.log 

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 

34__all__ = ("SingleFrameNaiveTrailConfig", "SingleFrameNaiveTrailPlugin") 

35 

36 

37class SingleFrameNaiveTrailConfig(SingleFramePluginConfig): 

38 """Config class for SingleFrameNaiveTrailPlugin. 

39 """ 

40 pass 

41 

42 

43@register("ext_trailedSources_Naive") 

44class SingleFrameNaiveTrailPlugin(SingleFramePlugin): 

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 

80 def getExecutionOrder(cls): 

81 # Needs centroids, shape, and flux measurements. 

82 # VeresPlugin is run after, which requires image data. 

83 return cls.APCORR_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.keyX0 = schema.addField(name + "_x0", type="D", doc="Trail head X coordinate.", units="pixel") 

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

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

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

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

94 self.keyL = schema.addField(name + "_length", type="D", doc="Trail length.", units="pixel") 

95 self.keyAngle = schema.addField(name + "_angle", type="D", doc="Angle measured from +x-axis.") 

96 

97 # Measurement Error Keys 

98 self.keyX0Err = schema.addField(name + "_x0Err", type="D", 

99 doc="Trail head X coordinate error.", units="pixel") 

100 self.keyY0Err = schema.addField(name + "_y0Err", type="D", 

101 doc="Trail head Y coordinate error.", units="pixel") 

102 self.keyX1Err = schema.addField(name + "_x1Err", type="D", 

103 doc="Trail tail X coordinate error.", units="pixel") 

104 self.keyY1Err = 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_FLUX = flagDefs.add("flag_noFlux", "No suitable prior flux measurement") 

110 self.NO_CONVERGE = flagDefs.add("flag_noConverge", "The root finder did not converge") 

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

112 

113 self.centriodExtractor = 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.centriodExtractor(measRecord, self.flagHandler) 

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.findLength(a2, sigma*sigma) 

139 if not results.converged: 

140 lsst.log.info(results.flag) 

141 raise MeasurementError(self.NO_CONVERGE.doc, self.NO_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_FLUX.doc, self.NO_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.keyX0, x0) 

182 measRecord.set(self.keyY0, y0) 

183 measRecord.set(self.keyX1, x1) 

184 measRecord.set(self.keyY1, y1) 

185 measRecord.set(self.keyFlux, flux) 

186 measRecord.set(self.keyL, length) 

187 measRecord.set(self.keyAngle, theta) 

188 measRecord.set(self.keyX0Err, x0Err) 

189 measRecord.set(self.keyY0Err, y0Err) 

190 measRecord.set(self.keyX1Err, x0Err) 

191 measRecord.set(self.keyY1Err, 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.flagHandler.handleFailure(measRecord) 

202 else: 

203 self.flagHandler.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(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