Coverage for python/lsst/afw/image/_exposureSummaryStats.py: 55%

146 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-19 04:04 -0700

1# This file is part of afw. 

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/>. 

21from __future__ import annotations 

22 

23import dataclasses 

24from typing import TYPE_CHECKING 

25import yaml 

26import warnings 

27 

28from ..typehandling import Storable, StorableHelperFactory 

29 

30if TYPE_CHECKING: 30 ↛ 31line 30 didn't jump to line 31, because the condition on line 30 was never true

31 from ..table import BaseRecord, Schema 

32 

33__all__ = ("ExposureSummaryStats", ) 

34 

35 

36def _default_corners(): 

37 return [float("nan")] * 4 

38 

39 

40@dataclasses.dataclass 

41class ExposureSummaryStats(Storable): 

42 _persistence_name = 'ExposureSummaryStats' 

43 

44 _factory = StorableHelperFactory(__name__, _persistence_name) 

45 

46 version: int = 0 

47 

48 psfSigma: float = float('nan') 

49 """PSF determinant radius (pixels).""" 

50 

51 psfArea: float = float('nan') 

52 """PSF effective area (pixels**2).""" 

53 

54 psfIxx: float = float('nan') 

55 """PSF shape Ixx (pixels**2).""" 

56 

57 psfIyy: float = float('nan') 

58 """PSF shape Iyy (pixels**2).""" 

59 

60 psfIxy: float = float('nan') 

61 """PSF shape Ixy (pixels**2).""" 

62 

63 ra: float = float('nan') 

64 """Bounding box center Right Ascension (degrees).""" 

65 

66 dec: float = float('nan') 

67 """Bounding box center Declination (degrees).""" 

68 

69 zenithDistance: float = float('nan') 

70 """Bounding box center zenith distance (degrees).""" 

71 

72 zeroPoint: float = float('nan') 

73 """Mean zeropoint in detector (mag).""" 

74 

75 skyBg: float = float('nan') 

76 """Average sky background (ADU).""" 

77 

78 skyNoise: float = float('nan') 

79 """Average sky noise (ADU).""" 

80 

81 meanVar: float = float('nan') 

82 """Mean variance of the weight plane (ADU**2).""" 

83 

84 raCorners: list[float] = dataclasses.field(default_factory=_default_corners) 

85 """Right Ascension of bounding box corners (degrees).""" 

86 

87 decCorners: list[float] = dataclasses.field(default_factory=_default_corners) 

88 """Declination of bounding box corners (degrees).""" 

89 

90 astromOffsetMean: float = float('nan') 

91 """Astrometry match offset mean.""" 

92 

93 astromOffsetStd: float = float('nan') 

94 """Astrometry match offset stddev.""" 

95 

96 nPsfStar: int = 0 

97 """Number of stars used for psf model.""" 

98 

99 psfStarDeltaE1Median: float = float('nan') 

100 """Psf stars median E1 residual (starE1 - psfE1).""" 

101 

102 psfStarDeltaE2Median: float = float('nan') 

103 """Psf stars median E2 residual (starE2 - psfE2).""" 

104 

105 psfStarDeltaE1Scatter: float = float('nan') 

106 """Psf stars MAD E1 scatter (starE1 - psfE1).""" 

107 

108 psfStarDeltaE2Scatter: float = float('nan') 

109 """Psf stars MAD E2 scatter (starE2 - psfE2).""" 

110 

111 psfStarDeltaSizeMedian: float = float('nan') 

112 """Psf stars median size residual (starSize - psfSize).""" 

113 

114 psfStarDeltaSizeScatter: float = float('nan') 

115 """Psf stars MAD size scatter (starSize - psfSize).""" 

116 

117 psfStarScaledDeltaSizeScatter: float = float('nan') 

118 """Psf stars MAD size scatter scaled by psfSize**2.""" 

119 

120 psfTraceRadiusDelta: float = float('nan') 

121 """Delta (max - min) of the model psf trace radius values evaluated on a 

122 grid of unmasked pixels (pixels). 

123 """ 

124 

125 maxDistToNearestPsf: float = float('nan') 

126 """Maximum distance of an unmasked pixel to its nearest model psf star 

127 (pixels). 

128 """ 

129 

130 effTime: float = float('nan') 

131 """Effective exposure time calculated from psfSigma, skyBg, and 

132 zeroPoint (seconds). 

133 """ 

134 

135 effTimePsfSigmaScale: float = float('nan') 

136 """PSF scaling of the effective exposure time.""" 

137 

138 effTimeSkyBgScale: float = float('nan') 

139 """Sky background scaling of the effective exposure time.""" 

140 

141 effTimeZeroPointScale: float = float('nan') 

142 """Zeropoint scaling of the effective exposure time.""" 

143 

144 def __post_init__(self): 

145 Storable.__init__(self) 

146 

147 def isPersistable(self): 

148 return True 

149 

150 def _getPersistenceName(self): 

151 return self._persistence_name 

152 

153 def _getPythonModule(self): 

154 return __name__ 

155 

156 def _write(self): 

157 return yaml.dump(dataclasses.asdict(self), encoding='utf-8') 

158 

159 @staticmethod 

160 def _read(bytes): 

161 yamlDict = yaml.load(bytes, Loader=yaml.SafeLoader) 

162 

163 # Special list of fields to forward to new names. 

164 forwardFieldDict = {"decl": "dec"} 

165 

166 # For forwards compatibility, filter out any fields that are 

167 # not defined in the dataclass. 

168 droppedFields = [] 

169 for _field in list(yamlDict.keys()): 

170 if _field not in ExposureSummaryStats.__dataclass_fields__: 

171 if _field in forwardFieldDict and forwardFieldDict[_field] not in yamlDict: 

172 yamlDict[forwardFieldDict[_field]] = yamlDict[_field] 

173 else: 

174 droppedFields.append(_field) 

175 yamlDict.pop(_field) 

176 if len(droppedFields) > 0: 

177 droppedFieldString = ", ".join([str(f) for f in droppedFields]) 

178 plural = "s" if len(droppedFields) != 1 else "" 

179 them = "them" if len(droppedFields) > 1 else "it" 

180 warnings.warn( 

181 f"Summary field{plural} [{droppedFieldString}] not recognized by this software version;" 

182 f" ignoring {them}.", 

183 FutureWarning, 

184 stacklevel=2, 

185 ) 

186 return ExposureSummaryStats(**yamlDict) 

187 

188 @classmethod 

189 def update_schema(cls, schema: Schema) -> None: 

190 """Update an schema to includes for all summary statistic fields. 

191 

192 Parameters 

193 ------- 

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

195 Schema to add which fields will be added. 

196 """ 

197 schema.addField( 

198 "psfSigma", 

199 type="F", 

200 doc="PSF model second-moments determinant radius (center of chip) (pixel)", 

201 units="pixel", 

202 ) 

203 schema.addField( 

204 "psfArea", 

205 type="F", 

206 doc="PSF model effective area (center of chip) (pixel**2)", 

207 units='pixel**2', 

208 ) 

209 schema.addField( 

210 "psfIxx", 

211 type="F", 

212 doc="PSF model Ixx (center of chip) (pixel**2)", 

213 units='pixel**2', 

214 ) 

215 schema.addField( 

216 "psfIyy", 

217 type="F", 

218 doc="PSF model Iyy (center of chip) (pixel**2)", 

219 units='pixel**2', 

220 ) 

221 schema.addField( 

222 "psfIxy", 

223 type="F", 

224 doc="PSF model Ixy (center of chip) (pixel**2)", 

225 units='pixel**2', 

226 ) 

227 schema.addField( 

228 "raCorners", 

229 type="ArrayD", 

230 size=4, 

231 doc="Right Ascension of bounding box corners (degrees)", 

232 units="degree", 

233 ) 

234 schema.addField( 

235 "decCorners", 

236 type="ArrayD", 

237 size=4, 

238 doc="Declination of bounding box corners (degrees)", 

239 units="degree", 

240 ) 

241 schema.addField( 

242 "ra", 

243 type="D", 

244 doc="Right Ascension of bounding box center (degrees)", 

245 units="degree", 

246 ) 

247 schema.addField( 

248 "dec", 

249 type="D", 

250 doc="Declination of bounding box center (degrees)", 

251 units="degree", 

252 ) 

253 schema.addField( 

254 "zenithDistance", 

255 type="F", 

256 doc="Zenith distance of bounding box center (degrees)", 

257 units="degree", 

258 ) 

259 schema.addField( 

260 "zeroPoint", 

261 type="F", 

262 doc="Mean zeropoint in detector (mag)", 

263 units="mag", 

264 ) 

265 schema.addField( 

266 "skyBg", 

267 type="F", 

268 doc="Average sky background (ADU)", 

269 units="adu", 

270 ) 

271 schema.addField( 

272 "skyNoise", 

273 type="F", 

274 doc="Average sky noise (ADU)", 

275 units="adu", 

276 ) 

277 schema.addField( 

278 "meanVar", 

279 type="F", 

280 doc="Mean variance of the weight plane (ADU**2)", 

281 units="adu**2" 

282 ) 

283 schema.addField( 

284 "astromOffsetMean", 

285 type="F", 

286 doc="Mean offset of astrometric calibration matches (arcsec)", 

287 units="arcsec", 

288 ) 

289 schema.addField( 

290 "astromOffsetStd", 

291 type="F", 

292 doc="Standard deviation of offsets of astrometric calibration matches (arcsec)", 

293 units="arcsec", 

294 ) 

295 schema.addField("nPsfStar", type="I", doc="Number of stars used for PSF model") 

296 schema.addField( 

297 "psfStarDeltaE1Median", 

298 type="F", 

299 doc="Median E1 residual (starE1 - psfE1) for psf stars", 

300 ) 

301 schema.addField( 

302 "psfStarDeltaE2Median", 

303 type="F", 

304 doc="Median E2 residual (starE2 - psfE2) for psf stars", 

305 ) 

306 schema.addField( 

307 "psfStarDeltaE1Scatter", 

308 type="F", 

309 doc="Scatter (via MAD) of E1 residual (starE1 - psfE1) for psf stars", 

310 ) 

311 schema.addField( 

312 "psfStarDeltaE2Scatter", 

313 type="F", 

314 doc="Scatter (via MAD) of E2 residual (starE2 - psfE2) for psf stars", 

315 ) 

316 schema.addField( 

317 "psfStarDeltaSizeMedian", 

318 type="F", 

319 doc="Median size residual (starSize - psfSize) for psf stars (pixel)", 

320 units="pixel", 

321 ) 

322 schema.addField( 

323 "psfStarDeltaSizeScatter", 

324 type="F", 

325 doc="Scatter (via MAD) of size residual (starSize - psfSize) for psf stars (pixel)", 

326 units="pixel", 

327 ) 

328 schema.addField( 

329 "psfStarScaledDeltaSizeScatter", 

330 type="F", 

331 doc="Scatter (via MAD) of size residual scaled by median size squared", 

332 ) 

333 schema.addField( 

334 "psfTraceRadiusDelta", 

335 type="F", 

336 doc="Delta (max - min) of the model psf trace radius values evaluated on a grid of " 

337 "unmasked pixels (pixel).", 

338 units="pixel", 

339 ) 

340 schema.addField( 

341 "maxDistToNearestPsf", 

342 type="F", 

343 doc="Maximum distance of an unmasked pixel to its nearest model psf star (pixel).", 

344 units="pixel", 

345 ) 

346 schema.addField( 

347 "effTime", 

348 type="F", 

349 doc="Effective exposure time calculated from psfSigma, skyBg, and " 

350 "zeroPoint (seconds).", 

351 units="second", 

352 ) 

353 schema.addField( 

354 "effTimePsfSigmaScale", 

355 type="F", 

356 doc="PSF scaling of the effective exposure time." 

357 ) 

358 schema.addField( 

359 "effTimeSkyBgScale", 

360 type="F", 

361 doc="Sky background scaling of the effective exposure time." 

362 ) 

363 schema.addField( 

364 "effTimeZeroPointScale", 

365 type="F", 

366 doc="Zeropoint scaling of the effective exposure time." 

367 ) 

368 

369 def update_record(self, record: BaseRecord) -> None: 

370 """Write summary-statistic columns into a record. 

371 

372 Parameters 

373 ---------- 

374 record : `lsst.afw.table.BaseRecord` 

375 Record to update. This is expected to frequently be an 

376 `ExposureRecord` instance (with higher-level code adding other 

377 columns and objects), but this method can work with any record 

378 type. 

379 """ 

380 for field in dataclasses.fields(self): 

381 value = getattr(self, field.name) 

382 if field.name == "version": 

383 continue 

384 elif field.type.startswith("list"): 

385 record[field.name][:] = value 

386 else: 

387 record[field.name] = value 

388 

389 @classmethod 

390 def from_record(cls, record: BaseRecord) -> ExposureSummaryStats: 

391 """Read summary-statistic columns from a record into ``self``. 

392 

393 Parameters 

394 ---------- 

395 record : `lsst.afw.table.BaseRecord` 

396 Record to read from. This is expected to frequently be an 

397 `ExposureRecord` instance (with higher-level code adding other 

398 columns and objects), but this method can work with any record 

399 type, ignoring any attributes or columns it doesn't recognize. 

400 

401 Returns 

402 ------- 

403 summary : `ExposureSummaryStats` 

404 Summary statistics object created from the given record. 

405 """ 

406 return cls( 

407 **{ 

408 field.name: ( 

409 record[field.name] if not field.type.startswith("list") 

410 else [float(v) for v in record[field.name]] 

411 ) 

412 for field in dataclasses.fields(cls) 

413 if field.name != "version" 

414 } 

415 )