Coverage for python / lsst / images / _observation_summary_stats.py: 67%

71 statements  

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

12 

13__all__ = ("ObservationSummaryStats",) 

14 

15import dataclasses 

16import math 

17from typing import Any, Self 

18 

19import pydantic 

20 

21 

22def _default_corners() -> tuple[float, float, float, float]: 

23 return (math.nan, math.nan, math.nan, math.nan) 

24 

25 

26class ObservationSummaryStats(pydantic.BaseModel, ser_json_inf_nan="constants"): 

27 version: int = pydantic.Field(0, description="Version of the model.") 

28 

29 psfSigma: float = pydantic.Field(math.nan, description="PSF determinant radius (pixels).") 

30 

31 psfArea: float = pydantic.Field(math.nan, description="PSF effective area (pixels**2).") 

32 

33 psfIxx: float = pydantic.Field(math.nan, description="PSF shape Ixx (pixels**2).") 

34 

35 psfIyy: float = pydantic.Field(math.nan, description="PSF shape Iyy (pixels**2).") 

36 

37 psfIxy: float = pydantic.Field(math.nan, description="PSF shape Ixy (pixels**2).") 

38 

39 ra: float = pydantic.Field(math.nan, description="Bounding box center Right Ascension (degrees).") 

40 

41 dec: float = pydantic.Field(math.nan, description="Bounding box center Declination (degrees).") 

42 

43 pixelScale: float = pydantic.Field(math.nan, description="Measured detector pixel scale (arcsec/pixel).") 

44 

45 zenithDistance: float = pydantic.Field( 

46 math.nan, description="Bounding box center zenith distance (degrees)." 

47 ) 

48 

49 expTime: float = pydantic.Field(math.nan, description="Exposure time of the exposure (seconds).") 

50 

51 zeroPoint: float = pydantic.Field(math.nan, description="Mean zeropoint in detector (mag).") 

52 

53 skyBg: float = pydantic.Field(math.nan, description="Average sky background (ADU).") 

54 

55 skyNoise: float = pydantic.Field(math.nan, description="Average sky noise (ADU).") 

56 

57 meanVar: float = pydantic.Field(math.nan, description="Mean variance of the weight plane (ADU**2).") 

58 

59 raCorners: tuple[float, float, float, float] = pydantic.Field( 

60 default_factory=_default_corners, description="Right Ascension of bounding box corners (degrees)." 

61 ) 

62 

63 decCorners: tuple[float, float, float, float] = pydantic.Field( 

64 default_factory=_default_corners, description="Declination of bounding box corners (degrees)." 

65 ) 

66 

67 astromOffsetMean: float = pydantic.Field(math.nan, description="Astrometry match offset mean.") 

68 

69 astromOffsetStd: float = pydantic.Field(math.nan, description="Astrometry match offset stddev.") 

70 

71 nPsfStar: int = pydantic.Field(0, description="Number of stars used for psf model.") 

72 

73 psfStarDeltaE1Median: float = pydantic.Field( 

74 math.nan, description="Psf stars median E1 residual (starE1 - psfE1)." 

75 ) 

76 

77 psfStarDeltaE2Median: float = pydantic.Field( 

78 math.nan, description="Psf stars median E2 residual (starE2 - psfE2)." 

79 ) 

80 

81 psfStarDeltaE1Scatter: float = pydantic.Field( 

82 math.nan, description="Psf stars MAD E1 scatter (starE1 - psfE1)." 

83 ) 

84 

85 psfStarDeltaE2Scatter: float = pydantic.Field( 

86 math.nan, description="Psf stars MAD E2 scatter (starE2 - psfE2)." 

87 ) 

88 

89 psfStarDeltaSizeMedian: float = pydantic.Field( 

90 math.nan, description="Psf stars median size residual (starSize - psfSize)." 

91 ) 

92 

93 psfStarDeltaSizeScatter: float = pydantic.Field( 

94 math.nan, description="Psf stars MAD size scatter (starSize - psfSize)." 

95 ) 

96 

97 psfStarScaledDeltaSizeScatter: float = pydantic.Field( 

98 math.nan, description="Psf stars MAD size scatter scaled by psfSize**2." 

99 ) 

100 

101 psfTraceRadiusDelta: float = pydantic.Field( 

102 math.nan, 

103 description=( 

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

105 "unmasked pixels (pixels)." 

106 ), 

107 ) 

108 

109 psfApFluxDelta: float = pydantic.Field( 

110 math.nan, 

111 description=( 

112 "Delta (max - min) of the model psf aperture flux (with aperture radius of max(2, 3*psfSigma)) " 

113 "values evaluated on a grid of unmasked pixels." 

114 ), 

115 ) 

116 

117 psfApCorrSigmaScaledDelta: float = pydantic.Field( 

118 math.nan, 

119 description=( 

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

121 "psfSigma evaluated on a grid of unmasked pixels." 

122 ), 

123 ) 

124 

125 maxDistToNearestPsf: float = pydantic.Field( 

126 math.nan, 

127 description="Maximum distance of an unmasked pixel to its nearest model psf star (pixels).", 

128 ) 

129 

130 starEMedian: float = pydantic.Field( 

131 math.nan, 

132 description=( 

133 "Median ellipticity (sqrt(starE1**2.0 + starE2**2.0)) of the stars used in the PSF model." 

134 ), 

135 ) 

136 

137 starUnNormalizedEMedian: float = pydantic.Field( 

138 math.nan, 

139 description=( 

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

141 "(2.0*starXY)**2.0)) of the stars used in the PSF model." 

142 ), 

143 ) 

144 

145 starComa1Median: float = pydantic.Field( 

146 math.nan, 

147 description=( 

148 "Coma-like higher-order moment combination: median M30 + M12 of the stars used in the PSF model." 

149 ), 

150 ) 

151 

152 starComa2Median: float = pydantic.Field( 

153 math.nan, 

154 description=( 

155 "Coma-like higher-order moment combination: median M21 + M03 of the stars used in the PSF model." 

156 ), 

157 ) 

158 

159 starTrefoil1Median: float = pydantic.Field( 

160 math.nan, 

161 description=( 

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

163 "of the stars used in the PSF model." 

164 ), 

165 ) 

166 

167 starTrefoil2Median: float = pydantic.Field( 

168 math.nan, 

169 description=( 

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

171 "of the stars used in the PSF model." 

172 ), 

173 ) 

174 

175 starKurtosisMedian: float = pydantic.Field( 

176 math.nan, 

177 description=( 

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

179 "of the stars used in the PSF model." 

180 ), 

181 ) 

182 

183 starE41Median: float = pydantic.Field( 

184 math.nan, 

185 description=( 

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

187 "of the stars used in the PSF model." 

188 ), 

189 ) 

190 

191 starE42Median: float = pydantic.Field( 

192 math.nan, 

193 description=( 

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

195 "of the stars used in the PSF model." 

196 ), 

197 ) 

198 

199 effTime: float = pydantic.Field( 

200 math.nan, 

201 description="Effective exposure time calculated from psfSigma, skyBg, and zeroPoint (seconds).", 

202 ) 

203 

204 effTimePsfSigmaScale: float = pydantic.Field( 

205 math.nan, description="PSF scaling of the effective exposure time." 

206 ) 

207 

208 effTimeSkyBgScale: float = pydantic.Field( 

209 math.nan, description="Sky background scaling of the effective exposure time." 

210 ) 

211 

212 effTimeZeroPointScale: float = pydantic.Field( 

213 math.nan, description="Zeropoint scaling of the effective exposure time." 

214 ) 

215 

216 magLim: float = pydantic.Field( 

217 math.nan, 

218 description=( 

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

220 " zeroPoint, and readNoise." 

221 ), 

222 ) 

223 

224 def __eq__(self, other: object) -> bool: 

225 if not isinstance(other, ObservationSummaryStats): 

226 return NotImplemented 

227 for name in self.model_fields: 

228 a = getattr(self, name) 

229 b = getattr(other, name) 

230 if isinstance(a, tuple) and isinstance(b, tuple): 

231 for ai, bi in zip(a, b): 

232 if ai != bi and not (math.isnan(ai) and math.isnan(bi)): 

233 return False 

234 elif a != b and not (math.isnan(a) and math.isnan(b)): 

235 return False 

236 return True 

237 

238 @classmethod 

239 def from_legacy(cls, exposure_summary_stats: Any) -> Self: 

240 """Return an `ObservationSummaryStats` from a legacy 

241 `lsst.afw.image.ExposureSummaryStats`. 

242 """ 

243 # Assume that all the fields in an ExposureSummaryStats dataclass 

244 # are compatible with an ObservationSummaryStats. 

245 summary_stats = dataclasses.asdict(exposure_summary_stats) 

246 return cls.model_validate(summary_stats)