Coverage for python / lsst / afw / image / _exposureSummaryStats.py: 56%
186 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-30 01:50 -0700
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-30 01:50 -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
23import dataclasses
24from typing import TYPE_CHECKING
25import yaml
26import warnings
28from ..typehandling import Storable, StorableHelperFactory
30if TYPE_CHECKING:
31 from ..table import BaseRecord, Schema
33__all__ = ("ExposureSummaryStats", )
36def _default_corners():
37 return [float("nan")] * 4
40@dataclasses.dataclass
41class ExposureSummaryStats(Storable):
42 _persistence_name = 'ExposureSummaryStats'
44 _factory = StorableHelperFactory(__name__, _persistence_name)
46 version: int = 0
48 psfSigma: float = float('nan')
49 """PSF determinant radius (pixels)."""
51 psfArea: float = float('nan')
52 """PSF effective area (pixels**2)."""
54 psfIxx: float = float('nan')
55 """PSF shape Ixx (pixels**2)."""
57 psfIyy: float = float('nan')
58 """PSF shape Iyy (pixels**2)."""
60 psfIxy: float = float('nan')
61 """PSF shape Ixy (pixels**2)."""
63 ra: float = float('nan')
64 """Bounding box center Right Ascension (degrees)."""
66 dec: float = float('nan')
67 """Bounding box center Declination (degrees)."""
69 pixelScale: float = float('nan')
70 """Measured detector pixel scale (arcsec/pixel)."""
72 zenithDistance: float = float('nan')
73 """Bounding box center zenith distance (degrees)."""
75 expTime: float = float('nan')
76 """Exposure time of the exposure (seconds)."""
78 zeroPoint: float = float('nan')
79 """Mean zeropoint in detector (mag)."""
81 skyBg: float = float('nan')
82 """Average sky background (ADU)."""
84 skyNoise: float = float('nan')
85 """Average sky noise (ADU)."""
87 meanVar: float = float('nan')
88 """Mean variance of the weight plane (ADU**2)."""
90 raCorners: list[float] = dataclasses.field(default_factory=_default_corners)
91 """Right Ascension of bounding box corners (degrees)."""
93 decCorners: list[float] = dataclasses.field(default_factory=_default_corners)
94 """Declination of bounding box corners (degrees)."""
96 astromOffsetMean: float = float('nan')
97 """Astrometry match offset mean."""
99 astromOffsetStd: float = float('nan')
100 """Astrometry match offset stddev."""
102 nPsfStar: int = 0
103 """Number of stars used for psf model."""
105 psfStarDeltaE1Median: float = float('nan')
106 """Psf stars median E1 residual (starE1 - psfE1)."""
108 psfStarDeltaE2Median: float = float('nan')
109 """Psf stars median E2 residual (starE2 - psfE2)."""
111 psfStarDeltaE1Scatter: float = float('nan')
112 """Psf stars MAD E1 scatter (starE1 - psfE1)."""
114 psfStarDeltaE2Scatter: float = float('nan')
115 """Psf stars MAD E2 scatter (starE2 - psfE2)."""
117 psfStarDeltaSizeMedian: float = float('nan')
118 """Psf stars median size residual (starSize - psfSize)."""
120 psfStarDeltaSizeScatter: float = float('nan')
121 """Psf stars MAD size scatter (starSize - psfSize)."""
123 psfStarScaledDeltaSizeScatter: float = float('nan')
124 """Psf stars MAD size scatter scaled by psfSize**2."""
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 """
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 """
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 """
141 maxDistToNearestPsf: float = float('nan')
142 """Maximum distance of an unmasked pixel to its nearest model psf star
143 (pixels).
144 """
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 """
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 """
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 """
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 """
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 """
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 """
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 """
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 """
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 """
192 effTime: float = float('nan')
193 """Effective exposure time calculated from psfSigma, skyBg, and
194 zeroPoint (seconds).
195 """
197 effTimePsfSigmaScale: float = float('nan')
198 """PSF scaling of the effective exposure time."""
200 effTimeSkyBgScale: float = float('nan')
201 """Sky background scaling of the effective exposure time."""
203 effTimeZeroPointScale: float = float('nan')
204 """Zeropoint scaling of the effective exposure time."""
206 magLim: float = float('nan')
207 """Magnitude limit at fixed SNR (default SNR=5) calculated from psfSigma, skyBg,
208 zeroPoint, and readNoise.
209 """
211 def __post_init__(self):
212 Storable.__init__(self)
214 def isPersistable(self):
215 return True
217 def _getPersistenceName(self):
218 return self._persistence_name
220 def _getPythonModule(self):
221 return __name__
223 def _write(self):
224 return yaml.dump(dataclasses.asdict(self), encoding='utf-8')
226 @staticmethod
227 def _read(bytes):
228 yamlDict = yaml.load(bytes, Loader=yaml.SafeLoader)
230 # Special list of fields to forward to new names.
231 forwardFieldDict = {"decl": "dec"}
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)
255 @classmethod
256 def update_schema(cls, schema: Schema) -> None:
257 """Update an schema to includes for all summary statistic fields.
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 )
521 def update_record(self, record: BaseRecord) -> None:
522 """Write summary-statistic columns into a record.
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
541 @classmethod
542 def from_record(cls, record: BaseRecord) -> ExposureSummaryStats:
543 """Read summary-statistic columns from a record into ``self``.
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.
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 )