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

214 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-13 01:23 -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 

28import numpy as np 

29 

30from ..typehandling import Storable, StorableHelperFactory 

31 

32if TYPE_CHECKING: 

33 from ..table import BaseRecord, Schema 

34 

35__all__ = ("ExposureSummaryStats", ) 

36 

37 

38def _default_corners(): 

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

40 

41 

42@dataclasses.dataclass 

43class ExposureSummaryStats(Storable): 

44 _persistence_name = 'ExposureSummaryStats' 

45 

46 _factory = StorableHelperFactory(__name__, _persistence_name) 

47 

48 version: int = 0 

49 

50 psfSigma: float = float('nan') 

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

52 

53 psfArea: float = float('nan') 

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

55 

56 psfIxx: float = float('nan') 

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

58 

59 psfIyy: float = float('nan') 

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

61 

62 psfIxy: float = float('nan') 

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

64 

65 ra: float = float('nan') 

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

67 

68 dec: float = float('nan') 

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

70 

71 pixelScale: float = float('nan') 

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

73 

74 zenithDistance: float = float('nan') 

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

76 

77 expTime: float = float('nan') 

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

79 

80 zeroPoint: float = float('nan') 

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

82 

83 skyBg: float = float('nan') 

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

85 

86 skyNoise: float = float('nan') 

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

88 

89 meanVar: float = float('nan') 

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

91 

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

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

94 

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

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

97 

98 psfAdaptiveThresholdValue: float = float('nan') 

99 """Threshold value used in the adaptive threshold detection pass for PSF modelling.""" 

100 

101 psfAdaptiveIncludeThresholdMultiplier: float = float('nan') 

102 """Threshold multiplier used in the adaptive threshold detection pass for PSF modelling.""" 

103 

104 nShapeletsStar: int = 0 

105 """Number of sources used in the shapelet decomposition.""" 

106 

107 shapeletsOnlyIqScore: float = float('nan') 

108 """The dimensionless image quality score as determined from the shapelets decomposition 

109 that includes power only from the non-atmospheric decomposition coefficients. The 

110 score spans the range [0.0, 1.0] with lower values indicating better image quality. 

111 """ 

112 

113 shapeletsIqScore: float = float('nan') 

114 """The dimensionless image quality score as determined from the shapelets decomposition 

115 that includes power from the median centroid offset between those used in the decomposition 

116 and those of the centroid slot in addition to non-atmospheric decomposition coefficients. 

117 The score spans the range [0.0, 1.0] with lower values indicating better image quality. 

118 """ 

119 

120 shapeletsCoeffs: list[float] = dataclasses.field(default_factory=list) 

121 """List of coefficients from the PSF star shapelet decomposition.""" 

122 

123 centroidDiffShapeletsVsSlotMedian: float = float('nan') 

124 """Median centroid difference (sqrt((slot_x - shapelet_x)**2 + (slot_y - shapelet_y)**2)) for 

125 sources used in the shapelet decomposition (pixels). 

126 """ 

127 

128 shapeletsStarEMedian: float = float('nan') 

129 """Median ellipticity (sqrt(starE1**2.0 + starE2**2.0)) of the sources used in the 

130 shapelet decomposition. 

131 """ 

132 

133 shapeletsStarUnNormalizedEMedian: float = float('nan') 

134 """Median un-normalized ellipticity (sqrt((starXX - starYY)**2.0 + (2.0*starXY)**2.0)) 

135 of the sources used in the shapelet decomposition (pixels**2). 

136 """ 

137 

138 astromOffsetMean: float = float('nan') 

139 """Astrometry match offset mean.""" 

140 

141 astromOffsetStd: float = float('nan') 

142 """Astrometry match offset stddev.""" 

143 

144 nPsfStar: int = 0 

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

146 

147 psfStarDeltaE1Median: float = float('nan') 

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

149 

150 psfStarDeltaE2Median: float = float('nan') 

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

152 

153 psfStarDeltaE1Scatter: float = float('nan') 

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

155 

156 psfStarDeltaE2Scatter: float = float('nan') 

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

158 

159 psfStarDeltaSizeMedian: float = float('nan') 

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

161 

162 psfStarDeltaSizeScatter: float = float('nan') 

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

164 

165 psfStarScaledDeltaSizeScatter: float = float('nan') 

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

167 

168 psfTraceRadiusDelta: float = float('nan') 

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

170 grid of unmasked pixels (pixels). 

171 """ 

172 

173 psfApFluxDelta: float = float('nan') 

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

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

176 """ 

177 

178 psfApCorrSigmaScaledDelta: float = float('nan') 

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

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

181 """ 

182 

183 maxDistToNearestPsf: float = float('nan') 

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

185 (pixels). 

186 """ 

187 

188 starEMedian: float = float('nan') 

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

190 in the PSF model. 

191 """ 

192 

193 starUnNormalizedEMedian: float = float('nan') 

194 """Median un-normalized ellipticity (sqrt((starXX - starYY)**2.0 + (2.0*starXY)**2.0)) 

195 of the stars used in the PSF model (pixel**2). 

196 """ 

197 

198 starComa1Median: float = float('nan') 

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

200 of the stars used in the PSF model. 

201 """ 

202 

203 starComa2Median: float = float('nan') 

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

205 of the stars used in the PSF model. 

206 """ 

207 

208 starTrefoil1Median: float = float('nan') 

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

210 of the stars used in the PSF model. 

211 """ 

212 

213 starTrefoil2Median: float = float('nan') 

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

215 of the stars used in the PSF model. 

216 """ 

217 

218 starKurtosisMedian: float = float('nan') 

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

220 of the stars used in the PSF model. 

221 """ 

222 

223 starE41Median: float = float('nan') 

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

225 of the stars used in the PSF model. 

226 """ 

227 

228 starE42Median: float = float('nan') 

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

230 of the stars used in the PSF model. 

231 """ 

232 

233 effTime: float = float('nan') 

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

235 zeroPoint (seconds). 

236 """ 

237 

238 effTimePsfSigmaScale: float = float('nan') 

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

240 

241 effTimeSkyBgScale: float = float('nan') 

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

243 

244 effTimeZeroPointScale: float = float('nan') 

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

246 

247 magLim: float = float('nan') 

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

249 zeroPoint, and readNoise. 

250 """ 

251 

252 def __post_init__(self): 

253 Storable.__init__(self) 

254 

255 def isPersistable(self): 

256 return True 

257 

258 def _getPersistenceName(self): 

259 return self._persistence_name 

260 

261 def _getPythonModule(self): 

262 return __name__ 

263 

264 def _write(self): 

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

266 

267 @staticmethod 

268 def _read(bytes): 

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

270 

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

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

273 

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

275 # not defined in the dataclass. 

276 droppedFields = [] 

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

278 if _field not in ExposureSummaryStats.__dataclass_fields__: 

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

280 yamlDict[forwardFieldDict[_field]] = yamlDict[_field] 

281 else: 

282 droppedFields.append(_field) 

283 yamlDict.pop(_field) 

284 if len(droppedFields) > 0: 

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

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

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

288 warnings.warn( 

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

290 f" ignoring {them}.", 

291 FutureWarning, 

292 stacklevel=2, 

293 ) 

294 return ExposureSummaryStats(**yamlDict) 

295 

296 @classmethod 

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

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

299 

300 Parameters 

301 ------- 

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

303 Schema to add which fields will be added. 

304 """ 

305 schema.addField( 

306 "psfSigma", 

307 type="F", 

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

309 units="pixel", 

310 ) 

311 schema.addField( 

312 "psfArea", 

313 type="F", 

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

315 units='pixel**2', 

316 ) 

317 schema.addField( 

318 "psfIxx", 

319 type="F", 

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

321 units='pixel**2', 

322 ) 

323 schema.addField( 

324 "psfIyy", 

325 type="F", 

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

327 units='pixel**2', 

328 ) 

329 schema.addField( 

330 "psfIxy", 

331 type="F", 

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

333 units='pixel**2', 

334 ) 

335 schema.addField( 

336 "raCorners", 

337 type="ArrayD", 

338 size=4, 

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

340 units="degree", 

341 ) 

342 schema.addField( 

343 "decCorners", 

344 type="ArrayD", 

345 size=4, 

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

347 units="degree", 

348 ) 

349 schema.addField( 

350 "ra", 

351 type="D", 

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

353 units="degree", 

354 ) 

355 schema.addField( 

356 "dec", 

357 type="D", 

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

359 units="degree", 

360 ) 

361 schema.addField( 

362 "zenithDistance", 

363 type="F", 

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

365 units="degree", 

366 ) 

367 schema.addField( 

368 "pixelScale", 

369 type="F", 

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

371 units="arcsec/pixel", 

372 ) 

373 schema.addField( 

374 "expTime", 

375 type="F", 

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

377 units="second", 

378 ) 

379 schema.addField( 

380 "zeroPoint", 

381 type="F", 

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

383 units="mag", 

384 ) 

385 schema.addField( 

386 "skyBg", 

387 type="F", 

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

389 units="adu", 

390 ) 

391 schema.addField( 

392 "skyNoise", 

393 type="F", 

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

395 units="adu", 

396 ) 

397 schema.addField( 

398 "meanVar", 

399 type="F", 

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

401 units="adu**2" 

402 ) 

403 schema.addField( 

404 "psfAdaptiveThresholdValue", 

405 type="F", 

406 doc="Threshold value used in the adaptive threshold detection pass for PSF modelling.", 

407 units="", 

408 ) 

409 schema.addField( 

410 "psfAdaptiveIncludeThresholdMultiplier", 

411 type="F", 

412 doc="Threshold multiplier used in the adaptive threshold detection pass for PSF modelling.", 

413 units="", 

414 ) 

415 schema.addField( 

416 "nShapeletsStar", 

417 type="I", 

418 doc="Number of sources used in the shapelet decomposition.", 

419 units="count", 

420 ) 

421 schema.addField( 

422 "shapeletsOnlyIqScore", 

423 type="F", 

424 doc="The dimensionless image quality score as determined from the shapelets " 

425 "decomposition that includes power only from the non-atmospheric decomposition " 

426 "coefficients. The score spans the range [0.0, 1.0] with lower values indicating " 

427 "better image quality.", 

428 units="", 

429 ) 

430 schema.addField( 

431 "shapeletsIqScore", 

432 type="F", 

433 doc="The dimensionless image quality score as determined from the shapelets " 

434 "decomposition that includes power from the median centroid offset between those " 

435 "used in the decomposition and those of the centroid slot in addition to " 

436 "non-atmospheric decomposition coefficients. The score spans the range [0.0, 1.0] " 

437 "with lower values indicating better image quality.", 

438 units="", 

439 ) 

440 schema.addField( 

441 "shapeletsCoeffs", 

442 type="ArrayD", 

443 size=0, # dynamic size 

444 doc="List of coefficients from the PSF star shapelet decomposition.", 

445 units="", 

446 ) 

447 schema.addField( 

448 "centroidDiffShapeletsVsSlotMedian", 

449 type="F", 

450 doc="Median centroid difference (sqrt((slot_x - shapelet_x)**2 + (slot_y - shapelet_y)**2)) " 

451 "for sources used in the shapelet decomposition.", 

452 units="pixel", 

453 ) 

454 schema.addField( 

455 "shapeletsStarEMedian", 

456 type="F", 

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

458 "shapelet decomposition.", 

459 units="", 

460 ) 

461 schema.addField( 

462 "shapeletsStarUnNormalizedEMedian", 

463 type="F", 

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

465 "of the stars used in the shapelet decomposition.", 

466 units="pixel**2", 

467 ) 

468 schema.addField( 

469 "astromOffsetMean", 

470 type="F", 

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

472 units="arcsec", 

473 ) 

474 schema.addField( 

475 "astromOffsetStd", 

476 type="F", 

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

478 units="arcsec", 

479 ) 

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

481 schema.addField( 

482 "psfStarDeltaE1Median", 

483 type="F", 

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

485 ) 

486 schema.addField( 

487 "psfStarDeltaE2Median", 

488 type="F", 

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

490 ) 

491 schema.addField( 

492 "psfStarDeltaE1Scatter", 

493 type="F", 

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

495 ) 

496 schema.addField( 

497 "psfStarDeltaE2Scatter", 

498 type="F", 

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

500 ) 

501 schema.addField( 

502 "psfStarDeltaSizeMedian", 

503 type="F", 

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

505 units="pixel", 

506 ) 

507 schema.addField( 

508 "psfStarDeltaSizeScatter", 

509 type="F", 

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

511 units="pixel", 

512 ) 

513 schema.addField( 

514 "psfStarScaledDeltaSizeScatter", 

515 type="F", 

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

517 ) 

518 schema.addField( 

519 "psfTraceRadiusDelta", 

520 type="F", 

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

522 "unmasked pixels (pixel).", 

523 units="pixel", 

524 ) 

525 schema.addField( 

526 "psfApFluxDelta", 

527 type="F", 

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

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

530 ) 

531 schema.addField( 

532 "psfApCorrSigmaScaledDelta", 

533 type="F", 

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

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

536 ) 

537 schema.addField( 

538 "maxDistToNearestPsf", 

539 type="F", 

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

541 units="pixel", 

542 ) 

543 schema.addField( 

544 "starEMedian", 

545 type="F", 

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

547 "the PSF model.", 

548 ) 

549 schema.addField( 

550 "starUnNormalizedEMedian", 

551 type="F", 

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

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

554 ) 

555 schema.addField( 

556 "starComa1Median", 

557 type="F", 

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

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

560 ) 

561 schema.addField( 

562 "starComa2Median", 

563 type="F", 

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

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

566 ) 

567 schema.addField( 

568 "starTrefoil1Median", 

569 type="F", 

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

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

572 ) 

573 schema.addField( 

574 "starTrefoil2Median", 

575 type="F", 

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

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

578 ) 

579 schema.addField( 

580 "starKurtosisMedian", 

581 type="F", 

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

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

584 ) 

585 schema.addField( 

586 "starE41Median", 

587 type="F", 

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

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

590 ) 

591 schema.addField( 

592 "starE42Median", 

593 type="F", 

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

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

596 ) 

597 schema.addField( 

598 "effTime", 

599 type="F", 

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

601 "zeroPoint (seconds).", 

602 units="second", 

603 ) 

604 schema.addField( 

605 "effTimePsfSigmaScale", 

606 type="F", 

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

608 ) 

609 schema.addField( 

610 "effTimeSkyBgScale", 

611 type="F", 

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

613 ) 

614 schema.addField( 

615 "effTimeZeroPointScale", 

616 type="F", 

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

618 ) 

619 schema.addField( 

620 "magLim", 

621 type="F", 

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

623 "skyBg, zeroPoint, and readNoise.", 

624 units="mag", 

625 ) 

626 

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

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

629 

630 Parameters 

631 ---------- 

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

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

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

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

636 type. 

637 """ 

638 for field in dataclasses.fields(self): 

639 value = getattr(self, field.name) 

640 if field.name == "version": 

641 continue 

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

643 record[field.name] = np.array(value, dtype=record[field.name].dtype) 

644 else: 

645 record[field.name] = value 

646 

647 @classmethod 

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

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

650 

651 Parameters 

652 ---------- 

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

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

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

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

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

658 

659 Returns 

660 ------- 

661 summary : `ExposureSummaryStats` 

662 Summary statistics object created from the given record. 

663 """ 

664 return cls( 

665 **{ 

666 field.name: ( 

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

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

669 ) 

670 for field in dataclasses.fields(cls) 

671 if field.name != "version" 

672 } 

673 )