Hide keyboard shortcuts

Hot-keys 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

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.flagHandler = FlagHandler.addFields(schema, name, flagDefs) 

115 

116 self.centriodExtractor = SafeCentroidExtractor(schema, name) 

117 

118 def measure(self, measRecord, exposure): 

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

120 

121 Parameters 

122 ---------- 

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

124 Record describing the object being measured. 

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

126 Pixel data to be measured. 

127 

128 See also 

129 -------- 

130 lsst.meas.base.SingleFramePlugin.measure 

131 """ 

132 

133 # Get the SdssShape centroid or fall back to slot 

134 xc = measRecord.get("base_SdssShape_x") 

135 yc = measRecord.get("base_SdssShape_y") 

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

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

138 

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

140 

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

142 xmy = Ixx - Iyy 

143 xpy = Ixx + Iyy 

144 xmy2 = xmy*xmy 

145 xy2 = Ixy*Ixy 

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

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

148 

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

150 if not results.converged: 

151 lsst.log.info(results.flag) 

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

153 

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

155 a = length/2.0 

156 dydt = a*np.cos(theta) 

157 dxdt = a*np.sin(theta) 

158 x0 = xc - dydt 

159 y0 = yc - dxdt 

160 x1 = xc + dydt 

161 y1 = yc + dxdt 

162 

163 # For now, use the shape flux. 

164 flux = measRecord.get("base_SdssShape_instFlux") 

165 

166 # Fall back to aperture flux 

167 if not np.isfinite(flux): 

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

169 flux = measRecord.getApInstFlux() 

170 else: 

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

172 

173 # Propagate errors from second moments 

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

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

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

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

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

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

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

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

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

183 

184 dxda = np.cos(theta) 

185 dyda = np.sin(theta) 

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

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

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

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

190 

191 # Set flags 

192 measRecord.set(self.keyRa, ra) 

193 measRecord.set(self.keyDec, dec) 

194 measRecord.set(self.keyX0, x0) 

195 measRecord.set(self.keyY0, y0) 

196 measRecord.set(self.keyX1, x1) 

197 measRecord.set(self.keyY1, y1) 

198 measRecord.set(self.keyFlux, flux) 

199 measRecord.set(self.keyL, length) 

200 measRecord.set(self.keyAngle, theta) 

201 measRecord.set(self.keyX0Err, x0Err) 

202 measRecord.set(self.keyY0Err, y0Err) 

203 measRecord.set(self.keyX1Err, x0Err) 

204 measRecord.set(self.keyY1Err, y0Err) 

205 

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

207 """Record failure 

208 

209 See also 

210 -------- 

211 lsst.meas.base.SingleFramePlugin.fail 

212 """ 

213 if error is None: 

214 self.flagHandler.handleFailure(measRecord) 

215 else: 

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

217 

218 def _computeSecondMomentDiff(self, z, c): 

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

220 

221 Parameters 

222 ---------- 

223 z : `float` 

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

225 c : `float` 

226 Constant (see notes) 

227 

228 Returns 

229 ------- 

230 diff : `float` 

231 Difference in numerical and analytic second moments. 

232 

233 Notes 

234 ----- 

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

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

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

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

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

240 finder. 

241 """ 

242 

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

244 return diff 

245 

246 def findLength(self, Ixx, Iyy): 

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

248 

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

250 the adaptive second-moments computed by previous measurements 

251 (ie. SdssShape). 

252 

253 Parameters 

254 ---------- 

255 Ixx : `float` 

256 Adaptive second-moment along x-axis. 

257 Iyy : `float` 

258 Adaptive second-moment along y-axis. 

259 

260 Returns 

261 ------- 

262 length : `float` 

263 Length of the trail. 

264 results : `scipy.optimize.RootResults` 

265 Contains messages about convergence from the root finder. 

266 """ 

267 

268 xpy = Ixx + Iyy 

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

270 

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

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

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

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

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

276 0.001, 1.0, full_output=True) 

277 

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

279 return length, results 

280 

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

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

283 

284 Parameters 

285 ---------- 

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

287 Exposure object containing the WCS. 

288 x : `float` 

289 x coordinate of the trail centroid 

290 y : `float` 

291 y coodinate of the trail centroid 

292 

293 Returns 

294 ------- 

295 ra : `float` 

296 Right ascension. 

297 dec : `float` 

298 Declination. 

299 """ 

300 

301 wcs = exposure.getWcs() 

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

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

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

305 return ra, dec