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

186 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-28 01:42 -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 starEMedian: float = float('nan') 

147 """Median ellipticity (sqrt(starE1**2.0 + starE2**2.0)) of the stars used 

148 in the PSF model. 

149 """ 

150 

151 starUnNormalizedEMedian: float = float('nan') 

152 """Median un-normalized ellipticity 

153 (sqrt((starXX - starYY)**2.0 + (2.0*starXY)**2.0)) 

154 of the stars used in the PSF model. 

155 """ 

156 

157 starComa1Median: float = float('nan') 

158 """Coma-like higher-order moment combination: median M30 + M12 

159 of the stars used in the PSF model. 

160 """ 

161 

162 starComa2Median: float = float('nan') 

163 """Coma-like higher-order moment combination: median M21 + M03 

164 of the stars used in the PSF model. 

165 """ 

166 

167 starTrefoil1Median: float = float('nan') 

168 """Trefoil-like higher-order moment combination: median M30 - 3*M12 

169 of the stars used in the PSF model. 

170 """ 

171 

172 starTrefoil2Median: float = float('nan') 

173 """Trefoil-like higher-order moment combination: median 3*M21 - M03 

174 of the stars used in the PSF model. 

175 """ 

176 

177 starKurtosisMedian: float = float('nan') 

178 """Kurtosis-like higher-order moment combination: median M40 + 2*M22 + M04 

179 of the stars used in the PSF model. 

180 """ 

181 

182 starE41Median: float = float('nan') 

183 """Fourth-order ellipticity-like higher-order moment combination: median M40 - M04 

184 of the stars used in the PSF model. 

185 """ 

186 

187 starE42Median: float = float('nan') 

188 """Fourth-order ellipticity-like higher-order moment combination: median 2*(M31 + M13) 

189 of the stars used in the PSF model. 

190 """ 

191 

192 effTime: float = float('nan') 

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

194 zeroPoint (seconds). 

195 """ 

196 

197 effTimePsfSigmaScale: float = float('nan') 

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

199 

200 effTimeSkyBgScale: float = float('nan') 

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

202 

203 effTimeZeroPointScale: float = float('nan') 

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

205 

206 magLim: float = float('nan') 

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

208 zeroPoint, and readNoise. 

209 """ 

210 

211 def __post_init__(self): 

212 Storable.__init__(self) 

213 

214 def isPersistable(self): 

215 return True 

216 

217 def _getPersistenceName(self): 

218 return self._persistence_name 

219 

220 def _getPythonModule(self): 

221 return __name__ 

222 

223 def _write(self): 

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

225 

226 @staticmethod 

227 def _read(bytes): 

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

229 

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

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

232 

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

234 # not defined in the dataclass. 

235 droppedFields = [] 

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

237 if _field not in ExposureSummaryStats.__dataclass_fields__: 

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

239 yamlDict[forwardFieldDict[_field]] = yamlDict[_field] 

240 else: 

241 droppedFields.append(_field) 

242 yamlDict.pop(_field) 

243 if len(droppedFields) > 0: 

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

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

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

247 warnings.warn( 

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

249 f" ignoring {them}.", 

250 FutureWarning, 

251 stacklevel=2, 

252 ) 

253 return ExposureSummaryStats(**yamlDict) 

254 

255 @classmethod 

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

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

258 

259 Parameters 

260 ------- 

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

262 Schema to add which fields will be added. 

263 """ 

264 schema.addField( 

265 "psfSigma", 

266 type="F", 

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

268 units="pixel", 

269 ) 

270 schema.addField( 

271 "psfArea", 

272 type="F", 

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

274 units='pixel**2', 

275 ) 

276 schema.addField( 

277 "psfIxx", 

278 type="F", 

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

280 units='pixel**2', 

281 ) 

282 schema.addField( 

283 "psfIyy", 

284 type="F", 

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

286 units='pixel**2', 

287 ) 

288 schema.addField( 

289 "psfIxy", 

290 type="F", 

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

292 units='pixel**2', 

293 ) 

294 schema.addField( 

295 "raCorners", 

296 type="ArrayD", 

297 size=4, 

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

299 units="degree", 

300 ) 

301 schema.addField( 

302 "decCorners", 

303 type="ArrayD", 

304 size=4, 

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

306 units="degree", 

307 ) 

308 schema.addField( 

309 "ra", 

310 type="D", 

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

312 units="degree", 

313 ) 

314 schema.addField( 

315 "dec", 

316 type="D", 

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

318 units="degree", 

319 ) 

320 schema.addField( 

321 "zenithDistance", 

322 type="F", 

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

324 units="degree", 

325 ) 

326 schema.addField( 

327 "pixelScale", 

328 type="F", 

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

330 units="arcsec/pixel", 

331 ) 

332 schema.addField( 

333 "expTime", 

334 type="F", 

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

336 units="second", 

337 ) 

338 schema.addField( 

339 "zeroPoint", 

340 type="F", 

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

342 units="mag", 

343 ) 

344 schema.addField( 

345 "skyBg", 

346 type="F", 

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

348 units="adu", 

349 ) 

350 schema.addField( 

351 "skyNoise", 

352 type="F", 

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

354 units="adu", 

355 ) 

356 schema.addField( 

357 "meanVar", 

358 type="F", 

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

360 units="adu**2" 

361 ) 

362 schema.addField( 

363 "astromOffsetMean", 

364 type="F", 

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

366 units="arcsec", 

367 ) 

368 schema.addField( 

369 "astromOffsetStd", 

370 type="F", 

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

372 units="arcsec", 

373 ) 

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

375 schema.addField( 

376 "psfStarDeltaE1Median", 

377 type="F", 

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

379 ) 

380 schema.addField( 

381 "psfStarDeltaE2Median", 

382 type="F", 

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

384 ) 

385 schema.addField( 

386 "psfStarDeltaE1Scatter", 

387 type="F", 

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

389 ) 

390 schema.addField( 

391 "psfStarDeltaE2Scatter", 

392 type="F", 

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

394 ) 

395 schema.addField( 

396 "psfStarDeltaSizeMedian", 

397 type="F", 

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

399 units="pixel", 

400 ) 

401 schema.addField( 

402 "psfStarDeltaSizeScatter", 

403 type="F", 

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

405 units="pixel", 

406 ) 

407 schema.addField( 

408 "psfStarScaledDeltaSizeScatter", 

409 type="F", 

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

411 ) 

412 schema.addField( 

413 "psfTraceRadiusDelta", 

414 type="F", 

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

416 "unmasked pixels (pixel).", 

417 units="pixel", 

418 ) 

419 schema.addField( 

420 "psfApFluxDelta", 

421 type="F", 

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

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

424 ) 

425 schema.addField( 

426 "psfApCorrSigmaScaledDelta", 

427 type="F", 

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

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

430 ) 

431 schema.addField( 

432 "maxDistToNearestPsf", 

433 type="F", 

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

435 units="pixel", 

436 ) 

437 schema.addField( 

438 "starEMedian", 

439 type="F", 

440 doc="Median ellipticity (sqrt(starE1**2.0 + starE2**2.0)) of the stars used in " 

441 "the PSF model.", 

442 ) 

443 schema.addField( 

444 "starUnNormalizedEMedian", 

445 type="F", 

446 doc="Median un-normalized ellipticity (sqrt((starXX - starYY)**2.0 + (2.0*starXY)**2.0)) " 

447 "of the stars used in the PSF model.", 

448 ) 

449 schema.addField( 

450 "starComa1Median", 

451 type="F", 

452 doc="Coma-like higher-order moment combination: median M30 + M12 " 

453 "of the stars used in the PSF model.", 

454 ) 

455 schema.addField( 

456 "starComa2Median", 

457 type="F", 

458 doc="Coma-like higher-order moment combination: median M21 + M03 " 

459 "of the stars used in the PSF model.", 

460 ) 

461 schema.addField( 

462 "starTrefoil1Median", 

463 type="F", 

464 doc="Trefoil-like higher-order moment combination: median M30 - 3*M12 " 

465 "of the stars used in the PSF model.", 

466 ) 

467 schema.addField( 

468 "starTrefoil2Median", 

469 type="F", 

470 doc="Trefoil-like higher-order moment combination: median 3*M21 - M03 " 

471 "of the stars used in the PSF model.", 

472 ) 

473 schema.addField( 

474 "starKurtosisMedian", 

475 type="F", 

476 doc="Kurtosis-like higher-order moment combination: median M40 + 2*M22 + M04 " 

477 "of the stars used in the PSF model.", 

478 ) 

479 schema.addField( 

480 "starE41Median", 

481 type="F", 

482 doc="Fourth-order ellipticity-like higher-order moment combination: median M40 - M04 " 

483 "of the stars used in the PSF model.", 

484 ) 

485 schema.addField( 

486 "starE42Median", 

487 type="F", 

488 doc="Fourth-order ellipticity-like higher-order moment combination: median 2*(M31 + M13) " 

489 "of the stars used in the PSF model.", 

490 ) 

491 schema.addField( 

492 "effTime", 

493 type="F", 

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

495 "zeroPoint (seconds).", 

496 units="second", 

497 ) 

498 schema.addField( 

499 "effTimePsfSigmaScale", 

500 type="F", 

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

502 ) 

503 schema.addField( 

504 "effTimeSkyBgScale", 

505 type="F", 

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

507 ) 

508 schema.addField( 

509 "effTimeZeroPointScale", 

510 type="F", 

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

512 ) 

513 schema.addField( 

514 "magLim", 

515 type="F", 

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

517 "skyBg, zeroPoint, and readNoise.", 

518 units="mag", 

519 ) 

520 

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

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

523 

524 Parameters 

525 ---------- 

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

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

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

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

530 type. 

531 """ 

532 for field in dataclasses.fields(self): 

533 value = getattr(self, field.name) 

534 if field.name == "version": 

535 continue 

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

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

538 else: 

539 record[field.name] = value 

540 

541 @classmethod 

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

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

544 

545 Parameters 

546 ---------- 

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

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

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

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

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

552 

553 Returns 

554 ------- 

555 summary : `ExposureSummaryStats` 

556 Summary statistics object created from the given record. 

557 """ 

558 return cls( 

559 **{ 

560 field.name: ( 

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

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

563 ) 

564 for field in dataclasses.fields(cls) 

565 if field.name != "version" 

566 } 

567 )