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

159 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-10-14 01:39 -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: 

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 pixelScale: float = float('nan') 

70 """Measured detector pixel scale (arcsec/pixel).""" 

71 

72 zenithDistance: float = float('nan') 

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

74 

75 expTime: float = float('nan') 

76 """Exposure time of the exposure (seconds).""" 

77 

78 zeroPoint: float = float('nan') 

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

80 

81 skyBg: float = float('nan') 

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

83 

84 skyNoise: float = float('nan') 

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

86 

87 meanVar: float = float('nan') 

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

89 

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

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

92 

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

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

95 

96 astromOffsetMean: float = float('nan') 

97 """Astrometry match offset mean.""" 

98 

99 astromOffsetStd: float = float('nan') 

100 """Astrometry match offset stddev.""" 

101 

102 nPsfStar: int = 0 

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

104 

105 psfStarDeltaE1Median: float = float('nan') 

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

107 

108 psfStarDeltaE2Median: float = float('nan') 

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

110 

111 psfStarDeltaE1Scatter: float = float('nan') 

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

113 

114 psfStarDeltaE2Scatter: float = float('nan') 

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

116 

117 psfStarDeltaSizeMedian: float = float('nan') 

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

119 

120 psfStarDeltaSizeScatter: float = float('nan') 

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

122 

123 psfStarScaledDeltaSizeScatter: float = float('nan') 

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

125 

126 psfTraceRadiusDelta: float = float('nan') 

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

128 grid of unmasked pixels (pixels). 

129 """ 

130 

131 psfApFluxDelta: float = float('nan') 

132 """Delta (max - min) of the model psf aperture flux (with aperture radius of 

133 max(2, 3*psfSigma)) values evaluated on a grid of unmasked pixels. 

134 """ 

135 

136 psfApCorrSigmaScaledDelta: float = float('nan') 

137 """Delta (max - min) of the psf flux aperture correction factors scaled (divided) 

138 by the psfSigma evaluated on a grid of unmasked pixels. 

139 """ 

140 

141 maxDistToNearestPsf: float = float('nan') 

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

143 (pixels). 

144 """ 

145 

146 effTime: float = float('nan') 

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

148 zeroPoint (seconds). 

149 """ 

150 

151 effTimePsfSigmaScale: float = float('nan') 

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

153 

154 effTimeSkyBgScale: float = float('nan') 

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

156 

157 effTimeZeroPointScale: float = float('nan') 

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

159 

160 magLim: float = float('nan') 

161 """Magnitude limit at fixed SNR (default SNR=5) calculated from psfSigma, skyBg, 

162 zeroPoint, and readNoise. 

163 """ 

164 

165 def __post_init__(self): 

166 Storable.__init__(self) 

167 

168 def isPersistable(self): 

169 return True 

170 

171 def _getPersistenceName(self): 

172 return self._persistence_name 

173 

174 def _getPythonModule(self): 

175 return __name__ 

176 

177 def _write(self): 

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

179 

180 @staticmethod 

181 def _read(bytes): 

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

183 

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

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

186 

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

188 # not defined in the dataclass. 

189 droppedFields = [] 

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

191 if _field not in ExposureSummaryStats.__dataclass_fields__: 

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

193 yamlDict[forwardFieldDict[_field]] = yamlDict[_field] 

194 else: 

195 droppedFields.append(_field) 

196 yamlDict.pop(_field) 

197 if len(droppedFields) > 0: 

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

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

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

201 warnings.warn( 

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

203 f" ignoring {them}.", 

204 FutureWarning, 

205 stacklevel=2, 

206 ) 

207 return ExposureSummaryStats(**yamlDict) 

208 

209 @classmethod 

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

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

212 

213 Parameters 

214 ------- 

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

216 Schema to add which fields will be added. 

217 """ 

218 schema.addField( 

219 "psfSigma", 

220 type="F", 

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

222 units="pixel", 

223 ) 

224 schema.addField( 

225 "psfArea", 

226 type="F", 

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

228 units='pixel**2', 

229 ) 

230 schema.addField( 

231 "psfIxx", 

232 type="F", 

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

234 units='pixel**2', 

235 ) 

236 schema.addField( 

237 "psfIyy", 

238 type="F", 

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

240 units='pixel**2', 

241 ) 

242 schema.addField( 

243 "psfIxy", 

244 type="F", 

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

246 units='pixel**2', 

247 ) 

248 schema.addField( 

249 "raCorners", 

250 type="ArrayD", 

251 size=4, 

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

253 units="degree", 

254 ) 

255 schema.addField( 

256 "decCorners", 

257 type="ArrayD", 

258 size=4, 

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

260 units="degree", 

261 ) 

262 schema.addField( 

263 "ra", 

264 type="D", 

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

266 units="degree", 

267 ) 

268 schema.addField( 

269 "dec", 

270 type="D", 

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

272 units="degree", 

273 ) 

274 schema.addField( 

275 "zenithDistance", 

276 type="F", 

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

278 units="degree", 

279 ) 

280 schema.addField( 

281 "pixelScale", 

282 type="F", 

283 doc="Measured detector pixel scale (arcsec/pixel)", 

284 units="arcsec/pixel", 

285 ) 

286 schema.addField( 

287 "expTime", 

288 type="F", 

289 doc="Exposure time of the exposure (seconds)", 

290 units="second", 

291 ) 

292 schema.addField( 

293 "zeroPoint", 

294 type="F", 

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

296 units="mag", 

297 ) 

298 schema.addField( 

299 "skyBg", 

300 type="F", 

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

302 units="adu", 

303 ) 

304 schema.addField( 

305 "skyNoise", 

306 type="F", 

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

308 units="adu", 

309 ) 

310 schema.addField( 

311 "meanVar", 

312 type="F", 

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

314 units="adu**2" 

315 ) 

316 schema.addField( 

317 "astromOffsetMean", 

318 type="F", 

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

320 units="arcsec", 

321 ) 

322 schema.addField( 

323 "astromOffsetStd", 

324 type="F", 

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

326 units="arcsec", 

327 ) 

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

329 schema.addField( 

330 "psfStarDeltaE1Median", 

331 type="F", 

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

333 ) 

334 schema.addField( 

335 "psfStarDeltaE2Median", 

336 type="F", 

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

338 ) 

339 schema.addField( 

340 "psfStarDeltaE1Scatter", 

341 type="F", 

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

343 ) 

344 schema.addField( 

345 "psfStarDeltaE2Scatter", 

346 type="F", 

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

348 ) 

349 schema.addField( 

350 "psfStarDeltaSizeMedian", 

351 type="F", 

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

353 units="pixel", 

354 ) 

355 schema.addField( 

356 "psfStarDeltaSizeScatter", 

357 type="F", 

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

359 units="pixel", 

360 ) 

361 schema.addField( 

362 "psfStarScaledDeltaSizeScatter", 

363 type="F", 

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

365 ) 

366 schema.addField( 

367 "psfTraceRadiusDelta", 

368 type="F", 

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

370 "unmasked pixels (pixel).", 

371 units="pixel", 

372 ) 

373 schema.addField( 

374 "psfApFluxDelta", 

375 type="F", 

376 doc="Delta (max - min) of the model psf aperture flux (with aperture radius of " 

377 "max(2, 3*psfSigma)) values evaluated on a grid of unmasked pixels.", 

378 ) 

379 schema.addField( 

380 "psfApCorrSigmaScaledDelta", 

381 type="F", 

382 doc="Delta (max - min) of the model psf aperture correction factors scaled (divided) " 

383 "by the psfSigma evaluated on a grid of unmasked pixels.", 

384 ) 

385 schema.addField( 

386 "maxDistToNearestPsf", 

387 type="F", 

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

389 units="pixel", 

390 ) 

391 schema.addField( 

392 "effTime", 

393 type="F", 

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

395 "zeroPoint (seconds).", 

396 units="second", 

397 ) 

398 schema.addField( 

399 "effTimePsfSigmaScale", 

400 type="F", 

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

402 ) 

403 schema.addField( 

404 "effTimeSkyBgScale", 

405 type="F", 

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

407 ) 

408 schema.addField( 

409 "effTimeZeroPointScale", 

410 type="F", 

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

412 ) 

413 schema.addField( 

414 "magLim", 

415 type="F", 

416 doc="Magnitude limit at SNR=5 (M5) calculated from psfSigma, " 

417 "skyBg, zeroPoint, and readNoise.", 

418 units="mag", 

419 ) 

420 

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

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

423 

424 Parameters 

425 ---------- 

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

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

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

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

430 type. 

431 """ 

432 for field in dataclasses.fields(self): 

433 value = getattr(self, field.name) 

434 if field.name == "version": 

435 continue 

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

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

438 else: 

439 record[field.name] = value 

440 

441 @classmethod 

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

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

444 

445 Parameters 

446 ---------- 

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

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

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

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

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

452 

453 Returns 

454 ------- 

455 summary : `ExposureSummaryStats` 

456 Summary statistics object created from the given record. 

457 """ 

458 return cls( 

459 **{ 

460 field.name: ( 

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

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

463 ) 

464 for field in dataclasses.fields(cls) 

465 if field.name != "version" 

466 } 

467 )