Coverage for python/lsst/analysis/tools/atools/calexpMetrics.py: 38%

54 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-24 10:05 +0000

1# This file is part of analysis_tools. 

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 

22 

23__all__ = ( 

24 "CalexpSummaryMetrics", 

25 "CalexpMetricHists", 

26 "AggregateVisitSummaryStats", 

27 "AggregateCornerSeparations", 

28) 

29 

30from lsst.pex.config import DictField 

31 

32from ..actions.plot import HistPanel, HistPlot 

33from ..actions.scalar import ( 

34 MaxAction, 

35 MedianAction, 

36 MinAction, 

37 SigmaMadAction, 

38) 

39from ..actions.vector import ( 

40 AngularSeparation, 

41 BandSelector, 

42 DivideVector, 

43 LoadVector, 

44) 

45from ..interfaces import AnalysisTool 

46 

47# Common to both CalexpSummaryMetrics and AggregateVisitSummaryStats. 

48# Units in comments indicate compound units, which are unsupported. 

49_SUMMARY_STATS_UNITS = { 

50 "psfSigma": "pixel", 

51 "psfArea": "", # pixel**2 

52 "psfIxx": "", # pixel**2 

53 "psfIyy": "", # pixel**2 

54 "psfIxy": "", # pixel**2 

55 "ra": "degree", 

56 "dec": "degree", 

57 "pixelScale": "", # arcsec/pixel. 

58 "zenithDistance": "degree", 

59 "expTime": "s", 

60 "zeroPoint": "mag", 

61 "skyBg": "electron", 

62 "skyNoise": "electron", 

63 "meanVar": "", # electron**2 

64 "astromOffsetMean": "arcsec", 

65 "astromOffsetStd": "arcsec", 

66 "nPsfStar": "ct", 

67 "psfStarDeltaE1Median": "", 

68 "psfStarDeltaE2Median": "", 

69 "psfStarDeltaE1Scatter": "", 

70 "psfStarDeltaE2Scatter": "", 

71 "psfStarDeltaSizeMedian": "pixel", 

72 "psfStarDeltaSizeScatter": "pixel", 

73 "psfStarScaledDeltaSizeScatter": "", 

74 "psfTraceRadiusDelta": "pixel", 

75 "psfApFluxDelta": "", 

76 "psfApCorrSigmaScaledDelta": "", 

77 "maxDistToNearestPsf": "pixel", 

78 "starEMedian": "", 

79 "starUnNormalizedEMedian": "", # pixel**2 

80 "effTime": "s", 

81 "effTimePsfSigmaScale": "", 

82 "effTimeSkyBgScale": "", 

83 "effTimeZeroPointScale": "", 

84 "magLim": "mag", 

85} 

86 

87 

88class CalexpSummaryMetrics(AnalysisTool): 

89 """ 

90 Class to load statistics from the summary stats contained with a calexp's 

91 metadata and write them to metrics. 

92 """ 

93 

94 propagateData: bool = True 

95 

96 _units = _SUMMARY_STATS_UNITS 

97 

98 def setDefaults(self): 

99 super().setDefaults() 

100 

101 self.prep.keysToLoad = list(self._units.keys()) 

102 self.produce.metric.units = self._units 

103 

104 

105class CalexpMetricHists(AnalysisTool): 

106 """ 

107 Class to generate histograms of metrics extracted from a Metrics Table. 

108 One plot per band. 

109 """ 

110 

111 parameterizedBand: bool = False 

112 metrics = DictField[str, str](doc="The metrics to plot and their respective labels.") 

113 

114 def setDefaults(self): 

115 super().setDefaults() 

116 

117 # Band is passed as a kwarg from the calling task. 

118 self.prep.selectors.bandSelector = BandSelector() 

119 self.produce.plot = HistPlot() 

120 

121 def finalize(self): 

122 

123 for metric, label in self.metrics.items(): 

124 setattr(self.process.buildActions, metric, LoadVector(vectorKey=metric)) 

125 self.produce.plot.panels[metric] = HistPanel(hists={metric: "Number of calexps"}, label=label) 

126 

127 

128class AggregateVisitSummaryStats(AnalysisTool): 

129 """ 

130 Class to calculate per-stat aggregates (min, max, median, sigmaMad) of 

131 vectors of summary stats. 

132 """ 

133 

134 _units = _SUMMARY_STATS_UNITS 

135 

136 aggregators = { 

137 "min": MinAction, 

138 "max": MaxAction, 

139 "median": MedianAction, 

140 "sigmaMad": SigmaMadAction, 

141 } 

142 

143 def finalize(self): 

144 units = {} 

145 for key in self._units.keys(): 

146 for agg_name, agg_cls in self.aggregators.items(): 

147 action = agg_cls() 

148 action.vectorKey = key 

149 setattr(self.process.calculateActions, f"{key}_{agg_name}", action) 

150 units[f"{key}_{agg_name}"] = self._units[key] 

151 self.produce.metric.units = units 

152 

153 

154class AggregateCornerSeparations(AnalysisTool): 

155 """ 

156 Class to calculate aggregate corner-to-corner separations 

157 from vectors of on-sky corner coordinates. 

158 """ 

159 

160 def setDefaults(self): 

161 super().setDefaults() 

162 

163 self.process.buildActions.cornersep = AngularSeparation( 

164 raKey_A="raCorners_0", 

165 decKey_A="decCorners_0", 

166 raKey_B="raCorners_2", 

167 decKey_B="decCorners_2", 

168 outputUnit="arcminute", 

169 ) 

170 self.process.buildActions.ratio = DivideVector() 

171 self.process.buildActions.ratio.actionA = AngularSeparation( 

172 raKey_A="raCorners_0", 

173 decKey_A="decCorners_0", 

174 raKey_B="raCorners_2", 

175 decKey_B="decCorners_2", 

176 outputUnit="arcminute", 

177 ) 

178 self.process.buildActions.ratio.actionB = AngularSeparation( 

179 raKey_A="raCorners_1", 

180 decKey_A="decCorners_1", 

181 raKey_B="raCorners_3", 

182 decKey_B="decCorners_3", 

183 outputUnit="arcminute", 

184 ) 

185 

186 self.process.calculateActions.cornerSeparation_min = MinAction(vectorKey="cornersep") 

187 self.process.calculateActions.cornerSeparation_max = MaxAction(vectorKey="cornersep") 

188 self.process.calculateActions.cornerSeparation_median = MedianAction(vectorKey="cornersep") 

189 self.process.calculateActions.cornerSeparation_sigmaMad = SigmaMadAction(vectorKey="cornersep") 

190 

191 self.process.calculateActions.cornerSeparationRatio_min = MinAction(vectorKey="ratio") 

192 self.process.calculateActions.cornerSeparationRatio_max = MaxAction(vectorKey="ratio") 

193 self.process.calculateActions.cornerSeparationRatio_median = MedianAction(vectorKey="ratio") 

194 self.process.calculateActions.cornerSeparationRatio_sigmaMad = SigmaMadAction(vectorKey="ratio") 

195 

196 self.produce.metric.units = { 

197 "cornerSeparation_min": "arcmin", 

198 "cornerSeparation_max": "arcmin", 

199 "cornerSeparation_median": "arcmin", 

200 "cornerSeparation_sigmaMad": "arcmin", 

201 "cornerSeparationRatio_min": "", 

202 "cornerSeparationRatio_max": "", 

203 "cornerSeparationRatio_median": "", 

204 "cornerSeparationRatio_sigmaMad": "", 

205 }