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

253 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-22 10:16 -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 refCatSourceDensity: float = float('nan') 

139 """Source density for the detector region as computed from the loaded reference catalog 

140 (number per degrees**2). 

141 """ 

142 

143 astromOffsetMean: float = float('nan') 

144 """Astrometry match offset mean.""" 

145 

146 astromOffsetStd: float = float('nan') 

147 """Astrometry match offset stddev.""" 

148 

149 nPsfStar: int = 0 

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

151 

152 psfStarDeltaE1Median: float = float('nan') 

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

154 

155 psfStarDeltaE2Median: float = float('nan') 

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

157 

158 psfStarDeltaE1Scatter: float = float('nan') 

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

160 

161 psfStarDeltaE2Scatter: float = float('nan') 

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

163 

164 psfStarDeltaSizeMedian: float = float('nan') 

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

166 

167 psfStarDeltaSizeScatter: float = float('nan') 

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

169 

170 psfStarScaledDeltaSizeScatter: float = float('nan') 

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

172 

173 psfTraceRadiusDelta: float = float('nan') 

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

175 grid of unmasked pixels (pixels). 

176 """ 

177 

178 psfApFluxDelta: float = float('nan') 

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

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

181 """ 

182 

183 psfApCorrSigmaScaledDelta: float = float('nan') 

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

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

186 """ 

187 

188 maxDistToNearestPsf: float = float('nan') 

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

190 (pixels). 

191 """ 

192 

193 starEMedian: float = float('nan') 

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

195 in the PSF model. 

196 """ 

197 

198 starUnNormalizedEMedian: float = float('nan') 

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

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

201 """ 

202 

203 starComa1Median: float = float('nan') 

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

205 of the stars used in the PSF model. 

206 """ 

207 

208 starComa2Median: float = float('nan') 

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

210 of the stars used in the PSF model. 

211 """ 

212 

213 starTrefoil1Median: float = float('nan') 

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

215 of the stars used in the PSF model. 

216 """ 

217 

218 starTrefoil2Median: float = float('nan') 

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

220 of the stars used in the PSF model. 

221 """ 

222 

223 starKurtosisMedian: float = float('nan') 

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

225 of the stars used in the PSF model. 

226 """ 

227 

228 starE41Median: float = float('nan') 

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

230 of the stars used in the PSF model. 

231 """ 

232 

233 starE42Median: float = float('nan') 

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

235 of the stars used in the PSF model. 

236 """ 

237 

238 effTime: float = float('nan') 

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

240 zeroPoint (seconds). 

241 """ 

242 

243 effTimePsfSigmaScale: float = float('nan') 

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

245 

246 effTimeSkyBgScale: float = float('nan') 

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

248 

249 effTimeZeroPointScale: float = float('nan') 

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

251 

252 magLim: float = float('nan') 

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

254 zeroPoint, and readNoise. 

255 """ 

256 

257 psfTE1e1: float = float('nan') 

258 """Per-exposure TE1e1 ~ <de1 de1> of PSF residual ellipticity, averaged over 

259 theta [0,1] arcmin via treecorr KK correlation. Dimensionless; used to form the 

260 full-survey TE1 metric. 

261 """ 

262 

263 psfTE1e2: float = float('nan') 

264 """Per-exposure TE1e2 ~ <de2 de2> of PSF residual ellipticity, averaged over 

265 theta [0,1] arcmin via treecorr KK correlation. Dimensionless; used to form the 

266 full-survey TE1 metric. 

267 """ 

268 

269 psfTE1ex: float = float('nan') 

270 """Per-exposure TE1ex ~ <de1 de2> of PSF residual ellipticity, averaged over 

271 theta [0,1] arcmin via treecorr KK correlation. Dimensionless; used to form the 

272 full-survey TE1 metric. 

273 """ 

274 

275 psfTE2e1: float = float('nan') 

276 """Per-exposure TE2e1 ~ <de1 de1> of PSF residual ellipticity, averaged over 

277 theta [5,100] arcmin via treecorr KK correlation. Dimensionless; used to form the 

278 full-survey TE2 metric. 

279 """ 

280 

281 psfTE2e2: float = float('nan') 

282 """Per-exposure TE2e2 ~ <de2 de2> of PSF residual ellipticity, averaged over 

283 theta [5,100] arcmin via treecorr KK correlation. Dimensionless; used to form the 

284 full-survey TE2 metric. 

285 """ 

286 

287 psfTE2ex: float = float('nan') 

288 """Per-exposure TE2ex ~ <de1 de2> of PSF residual ellipticity, averaged over 

289 theta [5,100] arcmin via treecorr KK correlation. Dimensionless; used to form the 

290 full-survey TE2 metric. 

291 """ 

292 

293 psfTE3e1: float = float('nan') 

294 """Per-exposure median-over-CCDs of TE3e1 ~ <de1 de1> of PSF residual 

295 ellipticity, where each CCD uses theta within [0,5] arcmin bins. Dimensionless; 

296 downstream pipelines take the 85th percentile over 

297 images to evaluate TE3. 

298 """ 

299 

300 psfTE3e2: float = float('nan') 

301 """Per-exposure median-over-CCDs of TE3e2 ~ <de2 de2> of PSF residual 

302 ellipticity, where each CCD uses theta within [0,5] arcmin bins. Dimensionless; 

303 downstream pipelines take the 85th percentile over 

304 images to evaluate TE3. 

305 """ 

306 

307 psfTE3ex: float = float('nan') 

308 """Per-exposure median-over-CCDs of TE3ex ~ <de1 de2> of PSF residual 

309 ellipticity, where each CCD uses theta within [0,5] arcmin bins. Dimensionless; 

310 downstream pipelines take the 85th percentile over 

311 images to evaluate TE3. 

312 """ 

313 

314 psfTE4e1: float = float('nan') 

315 """Per-exposure median-over-CCDs of TE4e1 ~ <de1 de1> of PSF residual 

316 ellipticity, where each CCD uses theta within [5,20] arcmin bins. Dimensionless; 

317 downstream pipelines take the 85th percentile over 

318 images to evaluate TE4. 

319 """ 

320 

321 psfTE4e2: float = float('nan') 

322 """Per-exposure median-over-CCDs of TE4e2 ~ <de2 de2> of PSF residual 

323 ellipticity, where each CCD uses theta within [5,20] arcmin bins. Dimensionless; 

324 downstream pipelines take the 85th percentile over 

325 images to evaluate TE4. 

326 """ 

327 

328 psfTE4ex: float = float('nan') 

329 """Per-exposure median-over-CCDs of TE4ex ~ <de1 de2> of PSF residual 

330 ellipticity, where each CCD uses theta within [5,20] arcmin bins. Dimensionless; 

331 downstream pipelines take the 85th percentile over 

332 images to evaluate TE4. 

333 """ 

334 

335 def __post_init__(self): 

336 Storable.__init__(self) 

337 

338 def isPersistable(self): 

339 return True 

340 

341 def _getPersistenceName(self): 

342 return self._persistence_name 

343 

344 def _getPythonModule(self): 

345 return __name__ 

346 

347 def _write(self): 

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

349 

350 @staticmethod 

351 def _read(bytes): 

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

353 

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

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

356 

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

358 # not defined in the dataclass. 

359 droppedFields = [] 

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

361 if _field not in ExposureSummaryStats.__dataclass_fields__: 

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

363 yamlDict[forwardFieldDict[_field]] = yamlDict[_field] 

364 else: 

365 droppedFields.append(_field) 

366 yamlDict.pop(_field) 

367 if len(droppedFields) > 0: 

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

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

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

371 warnings.warn( 

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

373 f" ignoring {them}.", 

374 FutureWarning, 

375 stacklevel=2, 

376 ) 

377 return ExposureSummaryStats(**yamlDict) 

378 

379 @classmethod 

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

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

382 

383 Parameters 

384 ------- 

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

386 Schema to add which fields will be added. 

387 """ 

388 schema.addField( 

389 "psfSigma", 

390 type="F", 

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

392 units="pixel", 

393 ) 

394 schema.addField( 

395 "psfArea", 

396 type="F", 

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

398 units='pixel**2', 

399 ) 

400 schema.addField( 

401 "psfIxx", 

402 type="F", 

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

404 units='pixel**2', 

405 ) 

406 schema.addField( 

407 "psfIyy", 

408 type="F", 

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

410 units='pixel**2', 

411 ) 

412 schema.addField( 

413 "psfIxy", 

414 type="F", 

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

416 units='pixel**2', 

417 ) 

418 schema.addField( 

419 "raCorners", 

420 type="ArrayD", 

421 size=4, 

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

423 units="degree", 

424 ) 

425 schema.addField( 

426 "decCorners", 

427 type="ArrayD", 

428 size=4, 

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

430 units="degree", 

431 ) 

432 schema.addField( 

433 "ra", 

434 type="D", 

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

436 units="degree", 

437 ) 

438 schema.addField( 

439 "dec", 

440 type="D", 

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

442 units="degree", 

443 ) 

444 schema.addField( 

445 "zenithDistance", 

446 type="F", 

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

448 units="degree", 

449 ) 

450 schema.addField( 

451 "pixelScale", 

452 type="F", 

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

454 units="arcsec/pixel", 

455 ) 

456 schema.addField( 

457 "expTime", 

458 type="F", 

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

460 units="second", 

461 ) 

462 schema.addField( 

463 "zeroPoint", 

464 type="F", 

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

466 units="mag", 

467 ) 

468 schema.addField( 

469 "skyBg", 

470 type="F", 

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

472 units="adu", 

473 ) 

474 schema.addField( 

475 "skyNoise", 

476 type="F", 

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

478 units="adu", 

479 ) 

480 schema.addField( 

481 "meanVar", 

482 type="F", 

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

484 units="adu**2" 

485 ) 

486 schema.addField( 

487 "psfAdaptiveThresholdValue", 

488 type="F", 

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

490 units="", 

491 ) 

492 schema.addField( 

493 "psfAdaptiveIncludeThresholdMultiplier", 

494 type="F", 

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

496 units="", 

497 ) 

498 schema.addField( 

499 "nShapeletsStar", 

500 type="I", 

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

502 units="count", 

503 ) 

504 schema.addField( 

505 "shapeletsOnlyIqScore", 

506 type="F", 

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

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

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

510 "better image quality.", 

511 units="", 

512 ) 

513 schema.addField( 

514 "shapeletsIqScore", 

515 type="F", 

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

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

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

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

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

521 units="", 

522 ) 

523 schema.addField( 

524 "shapeletsCoeffs", 

525 type="ArrayD", 

526 size=0, # dynamic size 

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

528 units="", 

529 ) 

530 schema.addField( 

531 "centroidDiffShapeletsVsSlotMedian", 

532 type="F", 

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

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

535 units="pixel", 

536 ) 

537 schema.addField( 

538 "shapeletsStarEMedian", 

539 type="F", 

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

541 "shapelet decomposition.", 

542 units="", 

543 ) 

544 schema.addField( 

545 "shapeletsStarUnNormalizedEMedian", 

546 type="F", 

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

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

549 units="pixel**2", 

550 ) 

551 schema.addField( 

552 "refCatSourceDensity", 

553 type="F", 

554 doc="Source density for the detector region as computed from the loaded reference catalog " 

555 "(number per degrees**2)", 

556 units="degree**-2", 

557 ) 

558 schema.addField( 

559 "astromOffsetMean", 

560 type="F", 

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

562 units="arcsec", 

563 ) 

564 schema.addField( 

565 "astromOffsetStd", 

566 type="F", 

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

568 units="arcsec", 

569 ) 

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

571 schema.addField( 

572 "psfStarDeltaE1Median", 

573 type="F", 

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

575 ) 

576 schema.addField( 

577 "psfStarDeltaE2Median", 

578 type="F", 

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

580 ) 

581 schema.addField( 

582 "psfStarDeltaE1Scatter", 

583 type="F", 

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

585 ) 

586 schema.addField( 

587 "psfStarDeltaE2Scatter", 

588 type="F", 

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

590 ) 

591 schema.addField( 

592 "psfStarDeltaSizeMedian", 

593 type="F", 

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

595 units="pixel", 

596 ) 

597 schema.addField( 

598 "psfStarDeltaSizeScatter", 

599 type="F", 

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

601 units="pixel", 

602 ) 

603 schema.addField( 

604 "psfStarScaledDeltaSizeScatter", 

605 type="F", 

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

607 ) 

608 schema.addField( 

609 "psfTraceRadiusDelta", 

610 type="F", 

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

612 "unmasked pixels (pixel).", 

613 units="pixel", 

614 ) 

615 schema.addField( 

616 "psfApFluxDelta", 

617 type="F", 

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

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

620 ) 

621 schema.addField( 

622 "psfApCorrSigmaScaledDelta", 

623 type="F", 

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

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

626 ) 

627 schema.addField( 

628 "maxDistToNearestPsf", 

629 type="F", 

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

631 units="pixel", 

632 ) 

633 schema.addField( 

634 "starEMedian", 

635 type="F", 

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

637 "the PSF model.", 

638 ) 

639 schema.addField( 

640 "starUnNormalizedEMedian", 

641 type="F", 

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

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

644 ) 

645 schema.addField( 

646 "starComa1Median", 

647 type="F", 

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

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

650 ) 

651 schema.addField( 

652 "starComa2Median", 

653 type="F", 

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

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

656 ) 

657 schema.addField( 

658 "starTrefoil1Median", 

659 type="F", 

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

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

662 ) 

663 schema.addField( 

664 "starTrefoil2Median", 

665 type="F", 

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

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

668 ) 

669 schema.addField( 

670 "starKurtosisMedian", 

671 type="F", 

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

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

674 ) 

675 schema.addField( 

676 "starE41Median", 

677 type="F", 

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

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

680 ) 

681 schema.addField( 

682 "starE42Median", 

683 type="F", 

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

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

686 ) 

687 schema.addField( 

688 "effTime", 

689 type="F", 

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

691 "zeroPoint (seconds).", 

692 units="second", 

693 ) 

694 schema.addField( 

695 "effTimePsfSigmaScale", 

696 type="F", 

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

698 ) 

699 schema.addField( 

700 "effTimeSkyBgScale", 

701 type="F", 

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

703 ) 

704 schema.addField( 

705 "effTimeZeroPointScale", 

706 type="F", 

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

708 ) 

709 schema.addField( 

710 "magLim", 

711 type="F", 

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

713 "skyBg, zeroPoint, and readNoise.", 

714 units="mag", 

715 ) 

716 schema.addField( 

717 "psfTE1e1", 

718 type="F", 

719 doc="Per-exposure E1e1 ~ <de1 de1> of PSF residual ellipticity " 

720 "over theta within [0,1] arcmin. Dimensionless; contributes to TE1.", 

721 ) 

722 schema.addField( 

723 "psfTE1e2", 

724 type="F", 

725 doc="Per-exposure E1e2 ~ <de2 de2> of PSF residual ellipticity " 

726 "over theta within [0,1] arcmin. Dimensionless; contributes to TE1.", 

727 ) 

728 schema.addField( 

729 "psfTE1ex", 

730 type="F", 

731 doc="Per-exposure E1ex ~ <de1 de2> of PSF residual ellipticity " 

732 "over theta within [0,1] arcmin. Dimensionless; contributes to TE1.", 

733 ) 

734 schema.addField( 

735 "psfTE2e1", 

736 type="F", 

737 doc="Per-exposure E2e1 ~ <de1 de1> of PSF residual ellipticity " 

738 "over theta within [5, 100] arcmin. Dimensionless; contributes to TE2.", 

739 ) 

740 schema.addField( 

741 "psfTE2e2", 

742 type="F", 

743 doc="Per-exposure E2e2 ~ <de2 de2> of PSF residual ellipticity " 

744 "over theta within [5, 100] arcmin. Dimensionless; contributes to TE2.", 

745 ) 

746 schema.addField( 

747 "psfTE2ex", 

748 type="F", 

749 doc="Per-exposure E2ex ~ <de1 de2> of PSF residual ellipticity " 

750 "over theta within [5, 100] arcmin. Dimensionless; contributes to TE2.", 

751 ) 

752 schema.addField( 

753 "psfTE3e1", 

754 type="F", 

755 doc="Per-exposure median-over-CCDs of TE3e1 ~ <de1 de1> with " 

756 "per-CCD theta within [0,5] arcmin. Dimensionless; used for TE3.", 

757 ) 

758 schema.addField( 

759 "psfTE3e2", 

760 type="F", 

761 doc="Per-exposure median-over-CCDs of TE3e2 ~ <de2 de2> with " 

762 "per-CCD theta within [0,5] arcmin. Dimensionless; used for TE3.", 

763 ) 

764 schema.addField( 

765 "psfTE3ex", 

766 type="F", 

767 doc="Per-exposure median-over-CCDs of TE3ex ~ <de1 de2> with " 

768 "per-CCD theta within [0,5] arcmin. Dimensionless; used for TE3.", 

769 ) 

770 schema.addField( 

771 "psfTE4e1", 

772 type="F", 

773 doc="Per-exposure median-over-CCDs of TE4e1 ~ <de1 de1> with " 

774 "per-CCD theta within [5, 20] arcmin. Dimensionless; used for TE4.", 

775 ) 

776 schema.addField( 

777 "psfTE4e2", 

778 type="F", 

779 doc="Per-exposure median-over-CCDs of TE4e2 ~ <de2 de2> with " 

780 "per-CCD theta within [5, 20] arcmin. Dimensionless; used for TE4.", 

781 ) 

782 schema.addField( 

783 "psfTE4ex", 

784 type="F", 

785 doc="Per-exposure median-over-CCDs of TE4ex ~ <de1 de2> with " 

786 "per-CCD theta within [5, 20] arcmin. Dimensionless; used for TE4.", 

787 ) 

788 

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

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

791 

792 Parameters 

793 ---------- 

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

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

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

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

798 type. 

799 """ 

800 for field in dataclasses.fields(self): 

801 value = getattr(self, field.name) 

802 if field.name == "version": 

803 continue 

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

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

806 else: 

807 record[field.name] = value 

808 

809 @classmethod 

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

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

812 

813 Parameters 

814 ---------- 

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

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

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

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

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

820 

821 Returns 

822 ------- 

823 summary : `ExposureSummaryStats` 

824 Summary statistics object created from the given record. 

825 """ 

826 return cls( 

827 **{ 

828 field.name: ( 

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

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

831 ) 

832 for field in dataclasses.fields(cls) 

833 if field.name != "version" 

834 } 

835 )