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

82 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-13 08:43 +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 psfAdaptiveThresholdValue: float = pydantic.Field( 

68 math.nan, 

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

70 ) 

71 

72 psfAdaptiveIncludeThresholdMultiplier: float = pydantic.Field( 

73 math.nan, 

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

75 ) 

76 

77 nShapeletsStar: int = pydantic.Field( 

78 0, 

79 description="Number of sources used in the shapelet decomposition.", 

80 ) 

81 

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 ) 

90 

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 ) 

100 

101 shapeletsCoeffs: tuple[float, ...] = pydantic.Field( 

102 default_factory=tuple, 

103 description="Coefficients from the PSF star shapelet decomposition.", 

104 ) 

105 

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 ) 

113 

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 ) 

121 

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 ) 

129 

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

131 

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

133 

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

135 

136 psfStarDeltaE1Median: float = pydantic.Field( 

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

138 ) 

139 

140 psfStarDeltaE2Median: float = pydantic.Field( 

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

142 ) 

143 

144 psfStarDeltaE1Scatter: float = pydantic.Field( 

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

146 ) 

147 

148 psfStarDeltaE2Scatter: float = pydantic.Field( 

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

150 ) 

151 

152 psfStarDeltaSizeMedian: float = pydantic.Field( 

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

154 ) 

155 

156 psfStarDeltaSizeScatter: float = pydantic.Field( 

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

158 ) 

159 

160 psfStarScaledDeltaSizeScatter: float = pydantic.Field( 

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

162 ) 

163 

164 psfTraceRadiusDelta: float = pydantic.Field( 

165 math.nan, 

166 description=( 

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

168 "unmasked pixels (pixels)." 

169 ), 

170 ) 

171 

172 psfApFluxDelta: float = pydantic.Field( 

173 math.nan, 

174 description=( 

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

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

177 ), 

178 ) 

179 

180 psfApCorrSigmaScaledDelta: float = pydantic.Field( 

181 math.nan, 

182 description=( 

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

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

185 ), 

186 ) 

187 

188 maxDistToNearestPsf: float = pydantic.Field( 

189 math.nan, 

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

191 ) 

192 

193 starEMedian: float = pydantic.Field( 

194 math.nan, 

195 description=( 

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

197 ), 

198 ) 

199 

200 starUnNormalizedEMedian: float = pydantic.Field( 

201 math.nan, 

202 description=( 

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

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

205 ), 

206 ) 

207 

208 starComa1Median: float = pydantic.Field( 

209 math.nan, 

210 description=( 

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

212 ), 

213 ) 

214 

215 starComa2Median: float = pydantic.Field( 

216 math.nan, 

217 description=( 

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

219 ), 

220 ) 

221 

222 starTrefoil1Median: float = pydantic.Field( 

223 math.nan, 

224 description=( 

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

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

227 ), 

228 ) 

229 

230 starTrefoil2Median: float = pydantic.Field( 

231 math.nan, 

232 description=( 

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

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

235 ), 

236 ) 

237 

238 starKurtosisMedian: float = pydantic.Field( 

239 math.nan, 

240 description=( 

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

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

243 ), 

244 ) 

245 

246 starE41Median: float = pydantic.Field( 

247 math.nan, 

248 description=( 

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

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

251 ), 

252 ) 

253 

254 starE42Median: float = pydantic.Field( 

255 math.nan, 

256 description=( 

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

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

259 ), 

260 ) 

261 

262 effTime: float = pydantic.Field( 

263 math.nan, 

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

265 ) 

266 

267 effTimePsfSigmaScale: float = pydantic.Field( 

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

269 ) 

270 

271 effTimeSkyBgScale: float = pydantic.Field( 

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

273 ) 

274 

275 effTimeZeroPointScale: float = pydantic.Field( 

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

277 ) 

278 

279 magLim: float = pydantic.Field( 

280 math.nan, 

281 description=( 

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

283 " zeroPoint, and readNoise." 

284 ), 

285 ) 

286 

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

288 if not isinstance(other, ObservationSummaryStats): 

289 return NotImplemented 

290 for name in self.model_fields: 

291 a = getattr(self, name) 

292 b = getattr(other, name) 

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

294 if len(a) != len(b): 

295 return False 

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

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

298 return False 

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

300 return False 

301 return True 

302 

303 @classmethod 

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

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

306 `lsst.afw.image.ExposureSummaryStats`. 

307 """ 

308 # Assume that all the fields in an ExposureSummaryStats dataclass 

309 # are compatible with an ObservationSummaryStats. 

310 summary_stats = dataclasses.asdict(exposure_summary_stats) 

311 return cls.model_validate(summary_stats)