Coverage for python/lsst/analysis/tools/analysisParts/shapeSizeFractional.py: 28%

128 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-08-06 09:59 +0000

1from __future__ import annotations 

2 

3from typing import cast 

4 

5import numpy as np 

6from lsst.analysis.tools.actions.vector import LoadVector, MagColumnNanoJansky, VectorSelector 

7from lsst.pex.config import Field 

8from lsst.pipe.tasks.configurableActions import ConfigurableActionField, ConfigurableActionStructField 

9 

10from ..actions.keyedData import AddComputedVector, KeyedScalars 

11from ..actions.plot.scatterplotWithTwoHists import ScatterPlotStatsAction 

12from ..actions.scalar import CountAction, MedianAction, SigmaMadAction 

13from ..actions.vector import ( 

14 CalcE, 

15 CalcEDiff, 

16 CalcShapeSize, 

17 CoaddPlotFlagSelector, 

18 DownselectVector, 

19 FractionalDifference, 

20 SnSelector, 

21 StarSelector, 

22) 

23from ..interfaces import ( 

24 AnalysisAction, 

25 AnalysisTool, 

26 KeyedData, 

27 KeyedDataAction, 

28 KeyedDataSchema, 

29 Scalar, 

30 ScalarAction, 

31 Vector, 

32 VectorAction, 

33) 

34 

35 

36class _ApproxMedian(ScalarAction): 

37 vectorKey = Field[str](doc="Key for the vector to perform action on", optional=False) 

38 

39 def getInputSchema(self) -> KeyedDataSchema: 

40 return ((self.vectorKey, Vector),) # type: ignore 

41 

42 def __call__(self, data: KeyedData, **kwargs) -> Scalar: 

43 mask = self.getMask(**kwargs) 

44 value = np.sort(data[self.vectorKey.format(**kwargs)][mask]) # type: ignore 

45 x = int(len(value) / 10) 

46 return np.nanmedian(value[-x:]) 

47 

48 

49class ShapeSizeFractionalScalars(KeyedScalars): 

50 vectorKey = Field[str](doc="Column key to compute scalars") 

51 

52 snFluxType = Field[str](doc="column key for the flux type used in SN selection") 

53 

54 selector = ConfigurableActionField[VectorAction](doc="Selector to use before computing Scalars") 

55 

56 def setDefaults(self): 

57 super().setDefaults() 

58 self.scalarActions.median = MedianAction(vectorKey=self.vectorKey) # type: ignore 

59 self.scalarActions.sigmaMad = SigmaMadAction(vectorKey=self.vectorKey) # type: ignore 

60 self.scalarActions.count = CountAction(vectorKey=self.vectorKey) # type: ignore 

61 self.scalarActions.approxMag = _ApproxMedian(vectorKey=self.snFluxType) # type: ignore 

62 

63 def __call__(self, data: KeyedData, **kwargs) -> KeyedData: 

64 mask = kwargs.get("mask") 

65 selection = self.selector(data, **kwargs) 

66 if mask is not None: 

67 mask &= selection 

68 else: 

69 mask = selection 

70 return super().__call__(data, **kwargs | dict(mask=mask)) 

71 

72 

73class ShapeSizeFractionalProcessLegacy(KeyedDataAction): 

74 psfFluxShape = ConfigurableActionField[AnalysisAction]( 

75 doc="Action to calculate the PSF Shape", 

76 ) 

77 shapeFracDif = ConfigurableActionField[AnalysisAction]( 

78 doc="Action which adds shapes to the KeyedData", 

79 ) 

80 objectSelector = ConfigurableActionField[AnalysisAction]( 

81 doc="Action to select which objects should be considered" 

82 ) 

83 highSNRSelector = ConfigurableActionField[AnalysisAction]( 

84 doc="Selector action add high SNR stars vector to output", 

85 ) 

86 lowSNRSelector = ConfigurableActionField[AnalysisAction]( 

87 doc="Selector action add low SNR stars vector to output", 

88 ) 

89 calculatorActions = ConfigurableActionStructField[KeyedDataAction]( 

90 doc="Actions which generate KeyedData of scalars", 

91 ) 

92 magAction = ConfigurableActionField(doc="Action to generate the magnitude column") 

93 

94 def setDefaults(self): 

95 # compute the PSF Shape 

96 psfShapeName = "{band}_psfShape" 

97 self.psfFluxShape = AddComputedVector() 

98 self.psfFluxShape.keyName = psfShapeName 

99 self.psfFluxShape.action = CalcShapeSize() 

100 self.psfFluxShape.action.colXx = "{band}_ixxPSF" 

101 self.psfFluxShape.action.colYy = "{band}_iyyPSF" 

102 self.psfFluxShape.action.colXy = "{band}_ixyPSF" 

103 

104 # compute the Difference of shapes 

105 self.shapeFracDif = AddComputedVector() 

106 self.shapeFracDif.keyName = "{band}_derivedShape" 

107 self.shapeFracDif.action = FractionalDifference() 

108 self.shapeFracDif.action.actionA = CalcShapeSize() 

109 self.shapeFracDif.action.actionB = LoadVector() 

110 self.shapeFracDif.action.actionB.vectorKey = psfShapeName 

111 

112 # Default mag action action 

113 self.magAction = MagColumnNanoJansky(vectorKey="{band}_psfFlux") 

114 

115 # Setup the selectors 

116 self.objectSelector = StarSelector() 

117 self.highSNRSelector = SnSelector(threshold=2700) 

118 self.lowSNRSelector = SnSelector(threshold=500) 

119 

120 self.calculatorActions.highSNStars = ShapeSizeFractionalScalars( 

121 vectorKey=self.shapeFracDif.keyName, snFluxType=self.highSNRSelector.fluxType 

122 ) 

123 highSNSelector = VectorSelector(vectorKey="starHighSNMask") 

124 self.calculatorActions.highSNStars.selector = highSNSelector 

125 self.calculatorActions.lowSNStars = ShapeSizeFractionalScalars( 

126 vectorKey=self.shapeFracDif.keyName, snFluxType=self.lowSNRSelector.fluxType 

127 ) 

128 self.calculatorActions.lowSNStars.selector = VectorSelector(vectorKey="starLowSNMask") 

129 

130 for action in self.calculatorActions: 

131 action.setDefaults() 

132 

133 super().setDefaults() 

134 

135 def getInputSchema(self) -> KeyedDataSchema: 

136 yield from self.psfFluxShape.getInputSchema() 

137 yield from self.objectSelector.getInputSchema() 

138 yield from self.highSNRSelector.getInputSchema() 

139 yield from self.lowSNRSelector.getInputSchema() 

140 yield from self.shapeFracDif.getInputSchema() 

141 for action in self.calculatorActions: 

142 yield from action.getInputSchema() 

143 

144 def getOutputSchema(self) -> KeyedDataSchema: 

145 results = ( 

146 ("starHighSNMask", Vector), 

147 ("starLowSNMask", Vector), 

148 ("xsStars", Vector), 

149 ("ysStars", Vector), 

150 ) 

151 for action in self.calculatorActions: 

152 if isinstance(action, KeyedDataAction): 

153 outputs = action.getOutputSchema() 

154 if outputs is not None: 

155 yield from outputs 

156 yield from results 

157 

158 def __call__(self, data: KeyedData, **kwargs) -> KeyedData: 

159 results = {} 

160 data = self.psfFluxShape(data, **kwargs) 

161 data = self.shapeFracDif(data, **kwargs) 

162 

163 objectMask = self.objectSelector(data, **kwargs) 

164 highSNRMask = self.highSNRSelector(data, **(kwargs | {"mask": objectMask})) 

165 lowSNRMask = self.lowSNRSelector(data, **kwargs | {"mask": objectMask}) 

166 

167 data["starHighSNMask"] = highSNRMask 

168 data["starLowSNMask"] = lowSNRMask 

169 

170 mags = self.magAction(data, **kwargs) # type: ignore 

171 

172 for name, action in self.calculatorActions.items(): # type: ignore 

173 for key, value in action(data, **kwargs).items(): 

174 prefix = f"{band}_" if (band := kwargs.get("band")) else "" 

175 newKey = f"{prefix}{name}_{key}" 

176 results[newKey] = value 

177 

178 results["starHighSNMask"] = highSNRMask[objectMask] 

179 results["starLowSNMask"] = lowSNRMask[objectMask] 

180 results["xsStars"] = mags[objectMask] 

181 shapeDiff = cast(Vector, data[cast(str, self.shapeFracDif.keyName.format(**kwargs))]) # type: ignore 

182 results["ysStars"] = shapeDiff[objectMask] 

183 return results 

184 

185 

186class BasePsfResidualMixin(AnalysisTool): 

187 """Shared configuration for `prep` and `process` stages of PSF residuals. 

188 

189 This is a mixin class used by `BasePsfResidualScatterPlot` and 

190 `BasePsfResidualMetric` to share common default configuration. 

191 """ 

192 

193 def setDefaults(self): 

194 super().setDefaults() 

195 self.prep.selectors.flagSelector = CoaddPlotFlagSelector() 

196 self.prep.selectors.snSelector = SnSelector(fluxType="{band}_psfFlux", threshold=100) 

197 

198 self.process.buildActions.mags = MagColumnNanoJansky(vectorKey="{band}_psfFlux") 

199 self.process.buildActions.fracDiff = FractionalDifference( 

200 actionA=CalcShapeSize(colXx="{band}_ixx", colYy="{band}_iyy", colXy="{band}_ixy"), 

201 actionB=CalcShapeSize(colXx="{band}_ixxPSF", colYy="{band}_iyyPSF", colXy="{band}_ixyPSF"), 

202 ) 

203 # Define an eDiff action and let e1Diff and e2Diff differ only in 

204 # component. 

205 self.process.buildActions.eDiff = CalcEDiff( 

206 colA=CalcE(colXx="{band}_ixx", colYy="{band}_iyy", colXy="{band}_ixy"), 

207 colB=CalcE(colXx="{band}_ixxPSF", colYy="{band}_iyyPSF", colXy="{band}_ixyPSF"), 

208 ) 

209 self.process.buildActions.e1Diff = self.process.buildActions.eDiff 

210 self.process.buildActions.e1Diff.component = "1" 

211 self.process.buildActions.e2Diff = self.process.buildActions.eDiff 

212 self.process.buildActions.e2Diff.component = "2" 

213 # pre-compute a stellar selector mask so it can be used in the filter 

214 # actions while only being computed once, alternatively the stellar 

215 # selector could be calculated and applied twice in the filter stage 

216 self.process.buildActions.starSelector = StarSelector() 

217 

218 self.process.filterActions.xStars = DownselectVector( 

219 vectorKey="mags", selector=VectorSelector(vectorKey="starSelector") 

220 ) 

221 # downselect the psfFlux as well 

222 self.process.filterActions.psfFlux = DownselectVector( 

223 vectorKey="{band}_psfFlux", selector=VectorSelector(vectorKey="starSelector") 

224 ) 

225 self.process.filterActions.psfFluxErr = DownselectVector( 

226 vectorKey="{band}_psfFluxErr", selector=VectorSelector(vectorKey="starSelector") 

227 ) 

228 

229 self.process.calculateActions.stars = ScatterPlotStatsAction( 

230 vectorKey="yStars", 

231 ) 

232 # use the downselected psfFlux 

233 self.process.calculateActions.stars.highSNSelector.fluxType = "psfFlux" 

234 self.process.calculateActions.stars.lowSNSelector.fluxType = "psfFlux" 

235 self.process.calculateActions.stars.fluxType = "psfFlux"