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

165 statements  

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

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

159 zeroPoint (seconds). 

160 """ 

161 

162 effTimePsfSigmaScale: float = float('nan') 

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

164 

165 effTimeSkyBgScale: float = float('nan') 

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

167 

168 effTimeZeroPointScale: float = float('nan') 

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

170 

171 magLim: float = float('nan') 

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

173 zeroPoint, and readNoise. 

174 """ 

175 

176 def __post_init__(self): 

177 Storable.__init__(self) 

178 

179 def isPersistable(self): 

180 return True 

181 

182 def _getPersistenceName(self): 

183 return self._persistence_name 

184 

185 def _getPythonModule(self): 

186 return __name__ 

187 

188 def _write(self): 

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

190 

191 @staticmethod 

192 def _read(bytes): 

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

194 

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

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

197 

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

199 # not defined in the dataclass. 

200 droppedFields = [] 

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

202 if _field not in ExposureSummaryStats.__dataclass_fields__: 

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

204 yamlDict[forwardFieldDict[_field]] = yamlDict[_field] 

205 else: 

206 droppedFields.append(_field) 

207 yamlDict.pop(_field) 

208 if len(droppedFields) > 0: 

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

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

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

212 warnings.warn( 

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

214 f" ignoring {them}.", 

215 FutureWarning, 

216 stacklevel=2, 

217 ) 

218 return ExposureSummaryStats(**yamlDict) 

219 

220 @classmethod 

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

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

223 

224 Parameters 

225 ------- 

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

227 Schema to add which fields will be added. 

228 """ 

229 schema.addField( 

230 "psfSigma", 

231 type="F", 

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

233 units="pixel", 

234 ) 

235 schema.addField( 

236 "psfArea", 

237 type="F", 

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

239 units='pixel**2', 

240 ) 

241 schema.addField( 

242 "psfIxx", 

243 type="F", 

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

245 units='pixel**2', 

246 ) 

247 schema.addField( 

248 "psfIyy", 

249 type="F", 

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

251 units='pixel**2', 

252 ) 

253 schema.addField( 

254 "psfIxy", 

255 type="F", 

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

257 units='pixel**2', 

258 ) 

259 schema.addField( 

260 "raCorners", 

261 type="ArrayD", 

262 size=4, 

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

264 units="degree", 

265 ) 

266 schema.addField( 

267 "decCorners", 

268 type="ArrayD", 

269 size=4, 

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

271 units="degree", 

272 ) 

273 schema.addField( 

274 "ra", 

275 type="D", 

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

277 units="degree", 

278 ) 

279 schema.addField( 

280 "dec", 

281 type="D", 

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

283 units="degree", 

284 ) 

285 schema.addField( 

286 "zenithDistance", 

287 type="F", 

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

289 units="degree", 

290 ) 

291 schema.addField( 

292 "pixelScale", 

293 type="F", 

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

295 units="arcsec/pixel", 

296 ) 

297 schema.addField( 

298 "expTime", 

299 type="F", 

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

301 units="second", 

302 ) 

303 schema.addField( 

304 "zeroPoint", 

305 type="F", 

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

307 units="mag", 

308 ) 

309 schema.addField( 

310 "skyBg", 

311 type="F", 

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

313 units="adu", 

314 ) 

315 schema.addField( 

316 "skyNoise", 

317 type="F", 

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

319 units="adu", 

320 ) 

321 schema.addField( 

322 "meanVar", 

323 type="F", 

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

325 units="adu**2" 

326 ) 

327 schema.addField( 

328 "astromOffsetMean", 

329 type="F", 

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

331 units="arcsec", 

332 ) 

333 schema.addField( 

334 "astromOffsetStd", 

335 type="F", 

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

337 units="arcsec", 

338 ) 

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

340 schema.addField( 

341 "psfStarDeltaE1Median", 

342 type="F", 

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

344 ) 

345 schema.addField( 

346 "psfStarDeltaE2Median", 

347 type="F", 

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

349 ) 

350 schema.addField( 

351 "psfStarDeltaE1Scatter", 

352 type="F", 

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

354 ) 

355 schema.addField( 

356 "psfStarDeltaE2Scatter", 

357 type="F", 

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

359 ) 

360 schema.addField( 

361 "psfStarDeltaSizeMedian", 

362 type="F", 

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

364 units="pixel", 

365 ) 

366 schema.addField( 

367 "psfStarDeltaSizeScatter", 

368 type="F", 

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

370 units="pixel", 

371 ) 

372 schema.addField( 

373 "psfStarScaledDeltaSizeScatter", 

374 type="F", 

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

376 ) 

377 schema.addField( 

378 "psfTraceRadiusDelta", 

379 type="F", 

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

381 "unmasked pixels (pixel).", 

382 units="pixel", 

383 ) 

384 schema.addField( 

385 "psfApFluxDelta", 

386 type="F", 

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

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

389 ) 

390 schema.addField( 

391 "psfApCorrSigmaScaledDelta", 

392 type="F", 

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

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

395 ) 

396 schema.addField( 

397 "maxDistToNearestPsf", 

398 type="F", 

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

400 units="pixel", 

401 ) 

402 schema.addField( 

403 "starEMedian", 

404 type="F", 

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

406 "the PSF model.", 

407 ) 

408 schema.addField( 

409 "starUnNormalizedEMedian", 

410 type="F", 

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

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

413 ) 

414 schema.addField( 

415 "effTime", 

416 type="F", 

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

418 "zeroPoint (seconds).", 

419 units="second", 

420 ) 

421 schema.addField( 

422 "effTimePsfSigmaScale", 

423 type="F", 

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

425 ) 

426 schema.addField( 

427 "effTimeSkyBgScale", 

428 type="F", 

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

430 ) 

431 schema.addField( 

432 "effTimeZeroPointScale", 

433 type="F", 

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

435 ) 

436 schema.addField( 

437 "magLim", 

438 type="F", 

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

440 "skyBg, zeroPoint, and readNoise.", 

441 units="mag", 

442 ) 

443 

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

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

446 

447 Parameters 

448 ---------- 

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

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

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

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

453 type. 

454 """ 

455 for field in dataclasses.fields(self): 

456 value = getattr(self, field.name) 

457 if field.name == "version": 

458 continue 

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

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

461 else: 

462 record[field.name] = value 

463 

464 @classmethod 

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

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

467 

468 Parameters 

469 ---------- 

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

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

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

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

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

475 

476 Returns 

477 ------- 

478 summary : `ExposureSummaryStats` 

479 Summary statistics object created from the given record. 

480 """ 

481 return cls( 

482 **{ 

483 field.name: ( 

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

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

486 ) 

487 for field in dataclasses.fields(cls) 

488 if field.name != "version" 

489 } 

490 )