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

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

120 statements  

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.geom import Point2D 

30from lsst.meas.base.pluginRegistry import register 

31from lsst.meas.base import SingleFramePlugin, SingleFramePluginConfig 

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

33from lsst.meas.base import MeasurementError 

34 

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

36 

37 

38class SingleFrameNaiveTrailConfig(SingleFramePluginConfig): 

39 """Config class for SingleFrameNaiveTrailPlugin. 

40 """ 

41 pass 

42 

43 

44@register("ext_trailedSources_Naive") 

45class SingleFrameNaiveTrailPlugin(SingleFramePlugin): 

46 """Naive trailed source measurement plugin 

47 

48 Measures the length, angle from +x-axis, and end points of an extended 

49 source using the second moments. 

50 

51 Parameters 

52 ---------- 

53 config: `SingleFrameNaiveTrailConfig` 

54 Plugin configuration. 

55 name: `str` 

56 Plugin name. 

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

58 Schema for the output catalog. 

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

60 Metadata to be attached to output catalog. 

61 

62 Notes 

63 ----- 

64 This measurement plugin aims to utilize the already measured adaptive 

65 second moments to naively estimate the length and angle, and thus 

66 end-points, of a fast-moving, trailed source. The length is solved for via 

67 finding the root of the difference between the numerical (stack computed) 

68 and the analytic adaptive second moments. The angle, theta, from the x-axis 

69 is also computed via adaptive moments: theta = arctan(2*Ixy/(Ixx - Iyy))/2. 

70 The end points of the trail are then given by (xc +/- (L/2)*cos(theta), 

71 yc +/- (L/2)*sin(theta)), with xc and yc being the centroid coordinates. 

72 

73 See also 

74 -------- 

75 lsst.meas.base.SingleFramePlugin 

76 """ 

77 

78 ConfigClass = SingleFrameNaiveTrailConfig 

79 

80 @classmethod 

81 def getExecutionOrder(cls): 

82 # Needs centroids, shape, and flux measurements. 

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

84 return cls.APCORR_ORDER + 0.1 

85 

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

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

88 

89 # Measurement Keys 

90 self.keyRa = schema.addField(name + "_ra", type="D", doc="Trail centroid right ascension.") 

91 self.keyDec = schema.addField(name + "_dec", type="D", doc="Trail centroid declination.") 

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

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

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

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

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

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

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

99 

100 # Measurement Error Keys 

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

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

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

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

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

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

107 self.keyY1Err = schema.addField(name + "_y1Err", type="D", 

108 doc="Trail tail Y coordinate error.", units="pixel") 

109 

110 flagDefs = FlagDefinitionList() 

111 flagDefs.addFailureFlag("No trailed-source measured") 

112 self.NO_FLUX = flagDefs.add("flag_noFlux", "No suitable prior flux measurement") 

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

114 self.NO_SIGMA = flagDefs.add("flag_noSigma", "No PSF width (sigma)") 

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

116 

117 self.centriodExtractor = SafeCentroidExtractor(schema, name) 

118 

119 def measure(self, measRecord, exposure): 

120 """Run the Naive trailed source measurement algorithm. 

121 

122 Parameters 

123 ---------- 

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

125 Record describing the object being measured. 

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

127 Pixel data to be measured. 

128 

129 See also 

130 -------- 

131 lsst.meas.base.SingleFramePlugin.measure 

132 """ 

133 

134 # Get the SdssShape centroid or fall back to slot 

135 xc = measRecord.get("base_SdssShape_x") 

136 yc = measRecord.get("base_SdssShape_y") 

137 if not np.isfinite(xc) or not np.isfinite(yc): 

138 xc, yc = self.centriodExtractor(measRecord, self.flagHandler) 

139 

140 ra, dec = self.computeRaDec(exposure, xc, yc) 

141 

142 Ixx, Iyy, Ixy = measRecord.getShape().getParameterVector() 

143 xmy = Ixx - Iyy 

144 xpy = Ixx + Iyy 

145 xmy2 = xmy*xmy 

146 xy2 = Ixy*Ixy 

147 a2 = 0.5 * (xpy + np.sqrt(xmy2 + 4.0*xy2)) 

148 

149 # Get the width of the PSF at the center of the trail 

150 center = Point2D(xc, yc) 

151 sigma = exposure.getPsf().computeShape(center).getTraceRadius() 

152 if not np.isfinite(sigma): 

153 raise MeasurementError(self.NO_SIGMA, self.NO_SIGMA.number) 

154 

155 length, results = self.findLength(a2, sigma*sigma) 

156 if not results.converged: 

157 lsst.log.info(results.flag) 

158 raise MeasurementError(self.NO_CONVERGE.doc, self.NO_CONVERGE.number) 

159 

160 theta = 0.5 * np.arctan2(2.0 * Ixy, xmy) 

161 a = length/2.0 

162 dydt = a*np.cos(theta) 

163 dxdt = a*np.sin(theta) 

164 x0 = xc - dydt 

165 y0 = yc - dxdt 

166 x1 = xc + dydt 

167 y1 = yc + dxdt 

168 

169 # For now, use the shape flux. 

170 flux = measRecord.get("base_SdssShape_instFlux") 

171 

172 # Fall back to aperture flux 

173 if not np.isfinite(flux): 

174 if np.isfinite(measRecord.getApInstFlux()): 

175 flux = measRecord.getApInstFlux() 

176 else: 

177 raise MeasurementError(self.NO_FLUX.doc, self.NO_FLUX.number) 

178 

179 # Propagate errors from second moments 

180 xcErr2, ycErr2 = np.diag(measRecord.getCentroidErr()) 

181 IxxErr2, IyyErr2, IxyErr2 = np.diag(measRecord.getShapeErr()) 

182 desc = np.sqrt(xmy2 + 4.0*xy2) # Descriminant^1/2 of EV equation 

183 denom = 2*np.sqrt(2.0*(Ixx + np.sqrt(4.0*xy2 + xmy2 + Iyy))) # Denominator for dadIxx and dadIyy 

184 dadIxx = (1.0 + (xmy/desc)) / denom 

185 dadIyy = (1.0 - (xmy/desc)) / denom 

186 dadIxy = (4.0*Ixy) / (desc * denom) 

187 aErr2 = IxxErr2*dadIxx*dadIxx + IyyErr2*dadIyy*dadIyy + IxyErr2*dadIxy*dadIxy 

188 thetaErr2 = ((IxxErr2 + IyyErr2)*xy2 + xmy2*IxyErr2) / (desc*desc*desc*desc) 

189 

190 dxda = np.cos(theta) 

191 dyda = np.sin(theta) 

192 xErr2 = aErr2*dxda*dxda + thetaErr2*dxdt*dxdt 

193 yErr2 = aErr2*dyda*dyda + thetaErr2*dydt*dydt 

194 x0Err = np.sqrt(xErr2 + xcErr2) # Same for x1 

195 y0Err = np.sqrt(yErr2 + ycErr2) # Same for y1 

196 

197 # Set flags 

198 measRecord.set(self.keyRa, ra) 

199 measRecord.set(self.keyDec, dec) 

200 measRecord.set(self.keyX0, x0) 

201 measRecord.set(self.keyY0, y0) 

202 measRecord.set(self.keyX1, x1) 

203 measRecord.set(self.keyY1, y1) 

204 measRecord.set(self.keyFlux, flux) 

205 measRecord.set(self.keyL, length) 

206 measRecord.set(self.keyAngle, theta) 

207 measRecord.set(self.keyX0Err, x0Err) 

208 measRecord.set(self.keyY0Err, y0Err) 

209 measRecord.set(self.keyX1Err, x0Err) 

210 measRecord.set(self.keyY1Err, y0Err) 

211 

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

213 """Record failure 

214 

215 See also 

216 -------- 

217 lsst.meas.base.SingleFramePlugin.fail 

218 """ 

219 if error is None: 

220 self.flagHandler.handleFailure(measRecord) 

221 else: 

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

223 

224 def _computeSecondMomentDiff(self, z, c): 

225 """Compute difference of the numerical and analytic second moments. 

226 

227 Parameters 

228 ---------- 

229 z : `float` 

230 Proportional to the length of the trail. (see notes) 

231 c : `float` 

232 Constant (see notes) 

233 

234 Returns 

235 ------- 

236 diff : `float` 

237 Difference in numerical and analytic second moments. 

238 

239 Notes 

240 ----- 

241 This is a simplified expression for the difference between the stack 

242 computed adaptive second-moment and the analytic solution. The variable 

243 z is proportional to the length such that L = 2*z*sqrt(2*(Ixx+Iyy)), 

244 and c is a constant (c = 4*Ixx/((Ixx+Iyy)*sqrt(pi))). Both have been 

245 defined to avoid unnecessary floating-point operations in the root 

246 finder. 

247 """ 

248 

249 diff = erf(z) - c*z*np.exp(-z*z) 

250 return diff 

251 

252 def findLength(self, Ixx, Iyy): 

253 """Find the length of a trail, given adaptive second-moments. 

254 

255 Uses a root finder to compute the length of a trail corresponding to 

256 the adaptive second-moments computed by previous measurements 

257 (ie. SdssShape). 

258 

259 Parameters 

260 ---------- 

261 Ixx : `float` 

262 Adaptive second-moment along x-axis. 

263 Iyy : `float` 

264 Adaptive second-moment along y-axis. 

265 

266 Returns 

267 ------- 

268 length : `float` 

269 Length of the trail. 

270 results : `scipy.optimize.RootResults` 

271 Contains messages about convergence from the root finder. 

272 """ 

273 

274 xpy = Ixx + Iyy 

275 c = 4.0*Ixx/(xpy*np.sqrt(np.pi)) 

276 

277 # Given a 'c' in (c_min, c_max], the root is contained in (0,1]. 

278 # c_min is given by the case: Ixx == Iyy, ie. a point source. 

279 # c_max is given by the limit Ixx >> Iyy. 

280 # Emperically, 0.001 is a suitable lower bound, assuming Ixx > Iyy. 

281 z, results = sciOpt.brentq(lambda z: self._computeSecondMomentDiff(z, c), 

282 0.001, 1.0, full_output=True) 

283 

284 length = 2.0*z*np.sqrt(2.0*xpy) 

285 return length, results 

286 

287 def computeRaDec(self, exposure, x, y): 

288 """Convert pixel coordinates to RA and Dec. 

289 

290 Parameters 

291 ---------- 

292 exposure : `lsst.afw.image.ExposureF` 

293 Exposure object containing the WCS. 

294 x : `float` 

295 x coordinate of the trail centroid 

296 y : `float` 

297 y coodinate of the trail centroid 

298 

299 Returns 

300 ------- 

301 ra : `float` 

302 Right ascension. 

303 dec : `float` 

304 Declination. 

305 """ 

306 

307 wcs = exposure.getWcs() 

308 center = wcs.pixelToSky(Point2D(x, y)) 

309 ra = center.getRa().asDegrees() 

310 dec = center.getDec().asDegrees() 

311 return ra, dec