Coverage for python / lsst / images / _observation_summary_stats.py: 68%
83 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-20 08:29 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-20 08:29 +0000
1# This file is part of lsst-images.
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# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
11from __future__ import annotations
13__all__ = ("ObservationSummaryStats",)
15import dataclasses
16import math
17from typing import Any, Self
19import pydantic
22def _default_corners() -> tuple[float, float, float, float]:
23 return (math.nan, math.nan, math.nan, math.nan)
26class ObservationSummaryStats(pydantic.BaseModel, ser_json_inf_nan="constants"):
27 version: int = pydantic.Field(0, description="Version of the model.")
29 psfSigma: float = pydantic.Field(math.nan, description="PSF determinant radius (pixels).")
31 psfArea: float = pydantic.Field(math.nan, description="PSF effective area (pixels**2).")
33 psfIxx: float = pydantic.Field(math.nan, description="PSF shape Ixx (pixels**2).")
35 psfIyy: float = pydantic.Field(math.nan, description="PSF shape Iyy (pixels**2).")
37 psfIxy: float = pydantic.Field(math.nan, description="PSF shape Ixy (pixels**2).")
39 ra: float = pydantic.Field(math.nan, description="Bounding box center Right Ascension (degrees).")
41 dec: float = pydantic.Field(math.nan, description="Bounding box center Declination (degrees).")
43 pixelScale: float = pydantic.Field(math.nan, description="Measured detector pixel scale (arcsec/pixel).")
45 zenithDistance: float = pydantic.Field(
46 math.nan, description="Bounding box center zenith distance (degrees)."
47 )
49 expTime: float = pydantic.Field(math.nan, description="Exposure time of the exposure (seconds).")
51 zeroPoint: float = pydantic.Field(math.nan, description="Mean zeropoint in detector (mag).")
53 skyBg: float = pydantic.Field(math.nan, description="Average sky background (ADU).")
55 skyNoise: float = pydantic.Field(math.nan, description="Average sky noise (ADU).")
57 meanVar: float = pydantic.Field(math.nan, description="Mean variance of the weight plane (ADU**2).")
59 raCorners: tuple[float, float, float, float] = pydantic.Field(
60 default_factory=_default_corners, description="Right Ascension of bounding box corners (degrees)."
61 )
63 decCorners: tuple[float, float, float, float] = pydantic.Field(
64 default_factory=_default_corners, description="Declination of bounding box corners (degrees)."
65 )
67 psfAdaptiveThresholdValue: float = pydantic.Field(
68 math.nan,
69 description="Threshold value used in the adaptive threshold detection pass for PSF modelling.",
70 )
72 psfAdaptiveIncludeThresholdMultiplier: float = pydantic.Field(
73 math.nan,
74 description="Threshold multiplier used in the adaptive threshold detection pass for PSF modelling.",
75 )
77 nShapeletsStar: int = pydantic.Field(
78 0,
79 description="Number of sources used in the shapelet decomposition.",
80 )
82 shapeletsOnlyIqScore: float = pydantic.Field(
83 math.nan,
84 description=(
85 "The dimensionless image quality score as determined from the shapelets decomposition "
86 "that includes power only from the non-atmospheric decomposition coefficients. The "
87 "score spans the range [0.0, 1.0] with lower values indicating better image quality."
88 ),
89 )
91 shapeletsIqScore: float = pydantic.Field(
92 math.nan,
93 description=(
94 "The dimensionless image quality score as determined from the shapelets decomposition "
95 "that includes power from the median centroid offset between those used in the decomposition "
96 "and those of the centroid slot in addition to non-atmospheric decomposition coefficients. "
97 "The score spans the range [0.0, 1.0] with lower values indicating better image quality."
98 ),
99 )
101 shapeletsCoeffs: tuple[float, ...] = pydantic.Field(
102 default_factory=tuple,
103 description="Coefficients from the PSF star shapelet decomposition.",
104 )
106 centroidDiffShapeletsVsSlotMedian: float = pydantic.Field(
107 math.nan,
108 description=(
109 "Median centroid difference (sqrt((slot_x - shapelet_x)**2 + (slot_y - shapelet_y)**2)) for "
110 "sources used in the shapelet decomposition (pixels)."
111 ),
112 )
114 shapeletsStarEMedian: float = pydantic.Field(
115 math.nan,
116 description=(
117 "Median ellipticity (sqrt(starE1**2.0 + starE2**2.0)) of the sources used in the "
118 "shapelet decomposition."
119 ),
120 )
122 shapeletsStarUnNormalizedEMedian: float = pydantic.Field(
123 math.nan,
124 description=(
125 "Median un-normalized ellipticity (sqrt((starXX - starYY)**2.0 + (2.0*starXY)**2.0)) "
126 "of the sources used in the shapelet decomposition (pixel**2)."
127 ),
128 )
130 refCatSourceDensity: float = pydantic.Field(
131 math.nan,
132 description=(
133 "Source density for the detector region as computed from the loaded reference catalog "
134 "(number per degrees**2)."
135 ),
136 )
138 astromOffsetMean: float = pydantic.Field(math.nan, description="Astrometry match offset mean.")
140 astromOffsetStd: float = pydantic.Field(math.nan, description="Astrometry match offset stddev.")
142 nPsfStar: int = pydantic.Field(0, description="Number of stars used for psf model.")
144 psfStarDeltaE1Median: float = pydantic.Field(
145 math.nan, description="Psf stars median E1 residual (starE1 - psfE1)."
146 )
148 psfStarDeltaE2Median: float = pydantic.Field(
149 math.nan, description="Psf stars median E2 residual (starE2 - psfE2)."
150 )
152 psfStarDeltaE1Scatter: float = pydantic.Field(
153 math.nan, description="Psf stars MAD E1 scatter (starE1 - psfE1)."
154 )
156 psfStarDeltaE2Scatter: float = pydantic.Field(
157 math.nan, description="Psf stars MAD E2 scatter (starE2 - psfE2)."
158 )
160 psfStarDeltaSizeMedian: float = pydantic.Field(
161 math.nan, description="Psf stars median size residual (starSize - psfSize)."
162 )
164 psfStarDeltaSizeScatter: float = pydantic.Field(
165 math.nan, description="Psf stars MAD size scatter (starSize - psfSize)."
166 )
168 psfStarScaledDeltaSizeScatter: float = pydantic.Field(
169 math.nan, description="Psf stars MAD size scatter scaled by psfSize**2."
170 )
172 psfTraceRadiusDelta: float = pydantic.Field(
173 math.nan,
174 description=(
175 "Delta (max - min) of the model psf trace radius values evaluated on a grid of "
176 "unmasked pixels (pixels)."
177 ),
178 )
180 psfApFluxDelta: float = pydantic.Field(
181 math.nan,
182 description=(
183 "Delta (max - min) of the model psf aperture flux (with aperture radius of max(2, 3*psfSigma)) "
184 "values evaluated on a grid of unmasked pixels."
185 ),
186 )
188 psfApCorrSigmaScaledDelta: float = pydantic.Field(
189 math.nan,
190 description=(
191 "Delta (max - min) of the psf flux aperture correction factors scaled (divided) by the "
192 "psfSigma evaluated on a grid of unmasked pixels."
193 ),
194 )
196 maxDistToNearestPsf: float = pydantic.Field(
197 math.nan,
198 description="Maximum distance of an unmasked pixel to its nearest model psf star (pixels).",
199 )
201 starEMedian: float = pydantic.Field(
202 math.nan,
203 description=(
204 "Median ellipticity (sqrt(starE1**2.0 + starE2**2.0)) of the stars used in the PSF model."
205 ),
206 )
208 starUnNormalizedEMedian: float = pydantic.Field(
209 math.nan,
210 description=(
211 "Median un-normalized ellipticity (sqrt((starXX - starYY)**2.0 + "
212 "(2.0*starXY)**2.0)) of the stars used in the PSF model."
213 ),
214 )
216 starComa1Median: float = pydantic.Field(
217 math.nan,
218 description=(
219 "Coma-like higher-order moment combination: median M30 + M12 of the stars used in the PSF model."
220 ),
221 )
223 starComa2Median: float = pydantic.Field(
224 math.nan,
225 description=(
226 "Coma-like higher-order moment combination: median M21 + M03 of the stars used in the PSF model."
227 ),
228 )
230 starTrefoil1Median: float = pydantic.Field(
231 math.nan,
232 description=(
233 "Trefoil-like higher-order moment combination: median M30 - 3*M12 "
234 "of the stars used in the PSF model."
235 ),
236 )
238 starTrefoil2Median: float = pydantic.Field(
239 math.nan,
240 description=(
241 "Trefoil-like higher-order moment combination: median 3*M21 - M03 "
242 "of the stars used in the PSF model."
243 ),
244 )
246 starKurtosisMedian: float = pydantic.Field(
247 math.nan,
248 description=(
249 "Kurtosis-like higher-order moment combination: median M40 + 2*M22 + M04 "
250 "of the stars used in the PSF model."
251 ),
252 )
254 starE41Median: float = pydantic.Field(
255 math.nan,
256 description=(
257 "Fourth-order ellipticity-like higher-order moment combination: median M40 - M04 "
258 "of the stars used in the PSF model."
259 ),
260 )
262 starE42Median: float = pydantic.Field(
263 math.nan,
264 description=(
265 "Fourth-order ellipticity-like higher-order moment combination: median 2*(M31 + M13) "
266 "of the stars used in the PSF model."
267 ),
268 )
270 effTime: float = pydantic.Field(
271 math.nan,
272 description="Effective exposure time calculated from psfSigma, skyBg, and zeroPoint (seconds).",
273 )
275 effTimePsfSigmaScale: float = pydantic.Field(
276 math.nan, description="PSF scaling of the effective exposure time."
277 )
279 effTimeSkyBgScale: float = pydantic.Field(
280 math.nan, description="Sky background scaling of the effective exposure time."
281 )
283 effTimeZeroPointScale: float = pydantic.Field(
284 math.nan, description="Zeropoint scaling of the effective exposure time."
285 )
287 magLim: float = pydantic.Field(
288 math.nan,
289 description=(
290 "Magnitude limit at fixed SNR (default SNR=5) calculated from psfSigma, skyBg,"
291 " zeroPoint, and readNoise."
292 ),
293 )
295 def __eq__(self, other: object) -> bool:
296 if not isinstance(other, ObservationSummaryStats):
297 return NotImplemented
298 for name in ObservationSummaryStats.model_fields:
299 a = getattr(self, name)
300 b = getattr(other, name)
301 if isinstance(a, tuple) and isinstance(b, tuple):
302 if len(a) != len(b):
303 return False
304 for ai, bi in zip(a, b):
305 if ai != bi and not (math.isnan(ai) and math.isnan(bi)):
306 return False
307 elif a != b and not (math.isnan(a) and math.isnan(b)):
308 return False
309 return True
311 @classmethod
312 def from_legacy(cls, exposure_summary_stats: Any) -> Self:
313 """Return an `ObservationSummaryStats` from a legacy
314 `lsst.afw.image.ExposureSummaryStats`.
315 """
316 # Assume that all the fields in an ExposureSummaryStats dataclass
317 # are compatible with an ObservationSummaryStats.
318 summary_stats = dataclasses.asdict(exposure_summary_stats)
319 return cls.model_validate(summary_stats)