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

83 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-15 01:54 -0700

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 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 ) 

137 

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

139 

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

141 

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

143 

144 psfStarDeltaE1Median: float = pydantic.Field( 

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

146 ) 

147 

148 psfStarDeltaE2Median: float = pydantic.Field( 

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

150 ) 

151 

152 psfStarDeltaE1Scatter: float = pydantic.Field( 

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

154 ) 

155 

156 psfStarDeltaE2Scatter: float = pydantic.Field( 

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

158 ) 

159 

160 psfStarDeltaSizeMedian: float = pydantic.Field( 

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

162 ) 

163 

164 psfStarDeltaSizeScatter: float = pydantic.Field( 

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

166 ) 

167 

168 psfStarScaledDeltaSizeScatter: float = pydantic.Field( 

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

170 ) 

171 

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 ) 

179 

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 ) 

187 

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 ) 

195 

196 maxDistToNearestPsf: float = pydantic.Field( 

197 math.nan, 

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

199 ) 

200 

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 ) 

207 

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 ) 

215 

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 ) 

222 

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 ) 

229 

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 ) 

237 

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 ) 

245 

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 ) 

253 

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 ) 

261 

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 ) 

269 

270 effTime: float = pydantic.Field( 

271 math.nan, 

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

273 ) 

274 

275 effTimePsfSigmaScale: float = pydantic.Field( 

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

277 ) 

278 

279 effTimeSkyBgScale: float = pydantic.Field( 

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

281 ) 

282 

283 effTimeZeroPointScale: float = pydantic.Field( 

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

285 ) 

286 

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 ) 

294 

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 

310 

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)