Coverage for python / lsst / ip / isr / flatGradient.py: 19%

110 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-18 08:54 +0000

1# This file is part of ip_isr. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21""" 

22Flat gradient fit storage class. 

23""" 

24 

25__all__ = ["FlatGradient"] 

26 

27from astropy.table import Table 

28import numpy as np 

29from scipy.interpolate import Akima1DInterpolator 

30 

31from lsst.ip.isr import IsrCalib 

32 

33 

34class FlatGradient(IsrCalib): 

35 """Flat gradient measurements. 

36 

37 Parameters 

38 ---------- 

39 log : `logging.Logger`, optional 

40 Log to write messages to. If `None` a default logger will be used. 

41 **kwargs : 

42 Additional parameters. 

43 """ 

44 

45 _OBSTYPE = "flatGradient" 

46 _SCHEMA = "FlatGradient" 

47 _VERSION = 1.0 

48 

49 def __init__(self, **kwargs): 

50 

51 self.radialSplineNodes = np.zeros(1) 

52 self.radialSplineValues = np.zeros(1) 

53 self.itlRatio = 1.0 

54 self.centroidX = 0.0 

55 self.centroidY = 0.0 

56 self.centroidDeltaX = 0.0 

57 self.centroidDeltaY = 0.0 

58 self.gradientX = 0.0 

59 self.gradientY = 0.0 

60 self.normalizationFactor = 1.0 

61 

62 super().__init__(**kwargs) 

63 

64 self.requiredAttributes.update( 

65 [ 

66 "radialSplineNodes", 

67 "radialSplineValues", 

68 "itlRatio", 

69 "centroidX", 

70 "centroidY", 

71 "centroidDeltaX", 

72 "centroidDeltaY", 

73 "gradientX", 

74 "gradientY", 

75 "normalizationFactor", 

76 ], 

77 ) 

78 

79 self.updateMetadata(setCalibInfo=True, setCalibId=True, **kwargs) 

80 

81 def setParameters( 

82 self, 

83 *, 

84 radialSplineNodes, 

85 radialSplineValues, 

86 itlRatio=1.0, 

87 centroidX=0.0, 

88 centroidY=0.0, 

89 centroidDeltaX=0.0, 

90 centroidDeltaY=0.0, 

91 gradientX=0.0, 

92 gradientY=0.0, 

93 normalizationFactor=1.0, 

94 ): 

95 """Set the parameters for the gradient model. 

96 

97 Parameters 

98 ---------- 

99 radialSplineNodes : `np.ndarray` 

100 Array of spline nodes. 

101 radialSplineValues : `np.ndarray` 

102 Array of spline values (same length as ``radialSplineNodes``). 

103 itlRatio : `float`, optional 

104 Ratio of flat for ITL detectors to E2V detectors. 

105 centroidX : `float`, optional 

106 X centroid of the focal plane (mm). This will be used as the 

107 pivot for the gradient plane. 

108 centroidY : `float`, optional 

109 Y centroid of the focal plane (mm). This will be used as the 

110 pivot for the gradient plane. 

111 centroidDeltaX : `float`, optional 

112 Centroid offset (mm). This is used in the radial function to 

113 allow for mis-centering in the illumination gradient. 

114 centroidDeltaY : `float`, optional 

115 Centroid offset (mm). This is used in the radial function to 

116 allow for mis-centering in the illumination gradient. 

117 gradientX : `float`, optional 

118 Slope of gradient in x direction (throughput/mm). 

119 gradientY : `float`, optional 

120 Slope of gradient in y direction (throughput/mm). 

121 normalizationFactor : `float`, optional 

122 Overall normalization factor (used to, e.g. make the 

123 center of the focal plane equal to 1.0 vs. a focal-plane 

124 average. 

125 """ 

126 if len(radialSplineNodes) != len(radialSplineValues): 

127 raise ValueError("The number of spline nodes and values must be equal.") 

128 

129 self.radialSplineNodes = radialSplineNodes 

130 self.radialSplineValues = radialSplineValues 

131 self.itlRatio = itlRatio 

132 self.centroidX = centroidX 

133 self.centroidY = centroidY 

134 self.centroidDeltaX = centroidDeltaX 

135 self.centroidDeltaY = centroidDeltaY 

136 self.gradientX = gradientX 

137 self.gradientY = gradientY 

138 self.normalizationFactor = normalizationFactor 

139 

140 def computeRadialSplineModelXY(self, x, y): 

141 """Compute the radial spline model values from x/y. 

142 

143 The spline model is a 1D Akima spline. When computed, the values 

144 from the model describe the radial function of the full focal 

145 plane flat-field. Dividing by this model will yield a radially 

146 flattened flat-field. 

147 

148 Parameters 

149 ---------- 

150 x : `np.ndarray` 

151 Array of focal plane x values (mm). 

152 y : `np.ndarray` 

153 Array of focal plane y values (mm). 

154 

155 Returns 

156 ------- 

157 splineModel : `np.ndarray` 

158 Spline model values at the x/y positions. 

159 """ 

160 centroidX = self.centroidX + self.centroidDeltaX 

161 centroidY = self.centroidY + self.centroidDeltaY 

162 

163 radius = np.sqrt((x - centroidX)**2. + (y - centroidY)**2.) 

164 

165 return self.computeRadialSplineModel(radius) 

166 

167 def computeRadialSplineModel(self, radius): 

168 """Compute the radial spline model values from radii. 

169 

170 The spline model is a 1D Akima spline. When computed, the values 

171 from the model describe the radial function of the full focal 

172 plane flat-field. Dividing by this model will yield a radially 

173 flattened flat-field. 

174 

175 Parameters 

176 ---------- 

177 radius : `np.ndarray` 

178 Array of focal plane radii (mm). 

179 

180 Returns 

181 ------- 

182 splineModel : `np.ndarray` 

183 Spline model values at the radius positions. 

184 """ 

185 spl = Akima1DInterpolator(self.radialSplineNodes, self.radialSplineValues) 

186 

187 return spl(np.clip(radius, self.radialSplineNodes[0], self.radialSplineNodes[-1])) 

188 

189 def computeGradientModel(self, x, y): 

190 """Compute the gradient model values. 

191 

192 The gradient model is a plane constrained to be 1.0 at the 

193 ``centroidX``, ``centroidY`` values. Dividing by this model will 

194 remove the planar gradient in a flat field. Note that the planar 

195 gradient pivot is always at the same position, and does not 

196 move with the radial gradient centroid so as to keep the 

197 model fit more stable. 

198 

199 Parameters 

200 ---------- 

201 x : `np.ndarray` 

202 Array of focal plane x values (mm). 

203 y : `np.ndarray` 

204 Array of focal plane y values (mm). 

205 

206 Returns 

207 ------- 

208 gradientModel : `np.ndarray` 

209 Gradient model values at the x/y positions. 

210 """ 

211 gradient = 1 + self.gradientX*(x - self.centroidX) + self.gradientY*(y - self.centroidY) 

212 

213 return gradient 

214 

215 def computeFullModel(self, x, y, is_itl): 

216 """Compute the full gradient model given x/y and itl booleans. 

217 

218 This returns the full model that can be applied directly 

219 to data that was used in a fit. 

220 

221 Parameters 

222 ---------- 

223 x : `np.ndarray` 

224 Array of focal plane x values (mm). 

225 y : `np.ndarray` 

226 Array of focal plane y values (mm). 

227 is_itl : `np.ndarray` 

228 Boolean array of whether each point is from an ITL detector. 

229 

230 Returns 

231 ------- 

232 model : `np.ndarray` 

233 Model values at each position. 

234 """ 

235 model = self.computeRadialSplineModelXY(x, y) / self.computeGradientModel(x, y) 

236 model[is_itl] *= self.itlRatio 

237 

238 return model 

239 

240 @classmethod 

241 def fromDict(cls, dictionary): 

242 """Construct a FlatGradient from a dictionary of properties. 

243 

244 Parameters 

245 ---------- 

246 dictionary : `dict` 

247 Dictionary of properties. 

248 

249 Returns 

250 ------- 

251 calib : `lsst.ip.isr.FlatGradient` 

252 Constructed calibration. 

253 """ 

254 calib = cls() 

255 

256 calib.setMetadata(dictionary["metadata"]) 

257 

258 calib.radialSplineNodes = np.asarray(dictionary["radialSplineNodes"]) 

259 calib.radialSplineValues = np.asarray(dictionary["radialSplineValues"]) 

260 calib.itlRatio = dictionary["itlRatio"] 

261 calib.centroidX = dictionary["centroidX"] 

262 calib.centroidY = dictionary["centroidY"] 

263 calib.centroidDeltaX = dictionary["centroidDeltaX"] 

264 calib.centroidDeltaY = dictionary["centroidDeltaY"] 

265 calib.gradientX = dictionary["gradientX"] 

266 calib.gradientY = dictionary["gradientY"] 

267 calib.normalizationFactor = dictionary["normalizationFactor"] 

268 

269 calib.updateMetadata() 

270 return calib 

271 

272 def toDict(self): 

273 """Return a dictionary containing the calibration properties. 

274 

275 Returns 

276 ------- 

277 dictionary : `dict` 

278 Dictionary of properties. 

279 """ 

280 self.updateMetadata() 

281 

282 outDict = dict() 

283 metadata = self.getMetadata() 

284 outDict["metadata"] = metadata 

285 

286 outDict["radialSplineNodes"] = self.radialSplineNodes.tolist() 

287 outDict["radialSplineValues"] = self.radialSplineValues.tolist() 

288 outDict["itlRatio"] = float(self.itlRatio) 

289 outDict["centroidX"] = float(self.centroidX) 

290 outDict["centroidY"] = float(self.centroidY) 

291 outDict["centroidDeltaX"] = float(self.centroidDeltaX) 

292 outDict["centroidDeltaY"] = float(self.centroidDeltaY) 

293 outDict["gradientX"] = float(self.gradientX) 

294 outDict["gradientY"] = float(self.gradientY) 

295 outDict["normalizationFactor"] = float(self.normalizationFactor) 

296 

297 return outDict 

298 

299 @classmethod 

300 def fromTable(cls, tableList): 

301 """Construct a calibration from a list of tables. 

302 

303 Parameters 

304 ---------- 

305 tableList : `list` [`astropy.table.Table`] 

306 List of table(s) to use to construct the FlatGradient. 

307 

308 Returns 

309 ------- 

310 calib : `lsst.ip.isr.FlatGradient` 

311 The calibration defined in the table(s). 

312 """ 

313 gradientTable = tableList[0] 

314 

315 metadata = gradientTable.meta 

316 inDict = dict() 

317 inDict["metadata"] = metadata 

318 inDict["radialSplineNodes"] = np.array(gradientTable[0]["RADIAL_SPLINE_NODES"], dtype=np.float64) 

319 inDict["radialSplineValues"] = np.array(gradientTable[0]["RADIAL_SPLINE_VALUES"], dtype=np.float64) 

320 inDict["itlRatio"] = float(gradientTable[0]["ITL_RATIO"][0]) 

321 inDict["centroidX"] = float(gradientTable[0]["CENTROID_X"][0]) 

322 inDict["centroidY"] = float(gradientTable[0]["CENTROID_Y"][0]) 

323 inDict["centroidDeltaX"] = float(gradientTable[0]["CENTROID_DELTA_X"][0]) 

324 inDict["centroidDeltaY"] = float(gradientTable[0]["CENTROID_DELTA_Y"][0]) 

325 inDict["gradientX"] = float(gradientTable[0]["GRADIENT_X"][0]) 

326 inDict["gradientY"] = float(gradientTable[0]["GRADIENT_Y"][0]) 

327 inDict["normalizationFactor"] = float(gradientTable[0]["NORMALIZATION_FACTOR"][0]) 

328 

329 return cls().fromDict(inDict) 

330 

331 def toTable(self): 

332 """Construct a list of table(s) containing the FlatGradient data. 

333 

334 Returns 

335 ------- 

336 tableList : `list` [`astropy.table.Table`] 

337 List of tables containing the FlatGradient information. 

338 """ 

339 tableList = [] 

340 self.updateMetadata() 

341 

342 catalog = Table( 

343 data=({ 

344 "RADIAL_SPLINE_NODES": self.radialSplineNodes, 

345 "RADIAL_SPLINE_VALUES": self.radialSplineValues, 

346 "ITL_RATIO": np.array([self.itlRatio]), 

347 "CENTROID_X": np.array([self.centroidX]), 

348 "CENTROID_Y": np.array([self.centroidY]), 

349 "CENTROID_DELTA_X": np.array([self.centroidDeltaX]), 

350 "CENTROID_DELTA_Y": np.array([self.centroidDeltaY]), 

351 "GRADIENT_X": np.array([self.gradientX]), 

352 "GRADIENT_Y": np.array([self.gradientY]), 

353 "NORMALIZATION_FACTOR": np.array([self.normalizationFactor]), 

354 },) 

355 ) 

356 

357 inMeta = self.getMetadata().toDict() 

358 outMeta = {k: v for k, v in inMeta.items() if v is not None} 

359 outMeta.update({k: "" for k, v in inMeta.items() if v is None}) 

360 catalog.meta = outMeta 

361 tableList.append(catalog) 

362 

363 return tableList