Coverage for python/lsst/pipe/tasks/scaleZeroPoint.py: 35%

85 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-03 01:34 -0700

1# This file is part of pipe_tasks. 

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/>. 

21 

22__all__ = ["ImageScaler", "SpatialImageScaler", "ScaleZeroPointTask"] 

23 

24import numpy 

25import lsst.geom as geom 

26import lsst.afw.image as afwImage 

27import lsst.pex.config as pexConfig 

28import lsst.pipe.base as pipeBase 

29from lsst.pipe.tasks.selectImages import BaseSelectImagesTask 

30 

31 

32class ImageScaler: 

33 """A class that scales an image. 

34 

35 This version uses a single scalar. Fancier versions may use a spatially varying scale. 

36 

37 Parameters 

38 ---------- 

39 scale : `float`, optional 

40 Scale correction to apply (see ``scaleMaskedImage``). 

41 """ 

42 

43 def __init__(self, scale=1.0): 

44 self._scale = scale 

45 

46 def scaleMaskedImage(self, maskedImage): 

47 """Scale the specified image or masked image in place. 

48 

49 Parameters 

50 ---------- 

51 maskedImage : `lsst.afw.image.MaskedImage` 

52 Masked image to scale. 

53 """ 

54 maskedImage *= self._scale 

55 

56 

57class SpatialImageScaler(ImageScaler): 

58 """Multiplicative image scaler using interpolation over a grid of points. 

59 

60 Contains the x, y positions in tract coordinates and the scale factors. 

61 Interpolates only when scaleMaskedImage() or getInterpImage() is called. 

62 

63 Currently the only type of 'interpolation' implemented is CONSTANT which calculates the mean. 

64 

65 Parameters 

66 ---------- 

67 interpStyle : `Unknown` 

68 Interpolation style (`CONSTANT` is only option). 

69 xList : `list` of `int` 

70 List of X pixel positions. 

71 yList : `list` of `int` 

72 List of Y pixel positions. 

73 scaleList : `Unknown` 

74 List of multiplicative scale factors at (x,y). 

75 

76 Raises 

77 ------ 

78 RuntimeError 

79 Raised if the lists have different lengths. 

80 """ 

81 

82 def __init__(self, interpStyle, xList, yList, scaleList): 

83 if len(xList) != len(yList) or len(xList) != len(scaleList): 

84 raise RuntimeError( 

85 "len(xList)=%s len(yList)=%s, len(scaleList)=%s but all lists must have the same length" % 

86 (len(xList), len(yList), len(scaleList))) 

87 

88 # Eventually want this do be: self.interpStyle = getattr(afwMath.Interpolate2D, interpStyle) 

89 self._xList = xList 

90 self._yList = yList 

91 self._scaleList = scaleList 

92 

93 def scaleMaskedImage(self, maskedImage): 

94 """Apply scale correction to the specified masked image. 

95 

96 Parameters 

97 ---------- 

98 image : `lsst.afw.image.MaskedImage` 

99 To scale; scale is applied in place. 

100 """ 

101 scale = self.getInterpImage(maskedImage.getBBox()) 

102 maskedImage *= scale 

103 

104 def getInterpImage(self, bbox): 

105 """Return an image containing the scale correction with same bounding box as supplied. 

106 

107 Parameters 

108 ---------- 

109 bbox : `lsst.geom.Box2I` 

110 Integer bounding box for image. 

111 

112 Raises 

113 ------ 

114 RuntimeError 

115 Raised if there are no fluxMag0s to interpolate. 

116 """ 

117 npoints = len(self._xList) 

118 

119 if npoints < 1: 

120 raise RuntimeError("Cannot create scaling image. Found no fluxMag0s to interpolate") 

121 

122 image = afwImage.ImageF(bbox, numpy.mean(self._scaleList)) 

123 

124 return image 

125 

126 

127class ScaleZeroPointConfig(pexConfig.Config): 

128 """Config for ScaleZeroPointTask. 

129 """ 

130 

131 zeroPoint = pexConfig.Field( 

132 dtype=float, 

133 doc="desired photometric zero point", 

134 default=27.0, 

135 ) 

136 

137 

138class SpatialScaleZeroPointConfig(ScaleZeroPointConfig): 

139 selectFluxMag0 = pexConfig.ConfigurableField( 

140 doc="Task to select data to compute spatially varying photometric zeropoint", 

141 target=BaseSelectImagesTask, 

142 ) 

143 

144 interpStyle = pexConfig.ChoiceField( 

145 dtype=str, 

146 doc="Algorithm to interpolate the flux scalings;" 

147 "Currently only one choice implemented", 

148 default="CONSTANT", 

149 allowed={ 

150 "CONSTANT": "Use a single constant value", 

151 } 

152 ) 

153 

154 

155class ScaleZeroPointTask(pipeBase.Task): 

156 """Compute scale factor to scale exposures to a desired photometric zero point. 

157 

158 This simple version assumes that the zero point is spatially invariant. 

159 """ 

160 

161 ConfigClass = ScaleZeroPointConfig 

162 _DefaultName = "scaleZeroPoint" 

163 

164 def __init__(self, *args, **kwargs): 

165 pipeBase.Task.__init__(self, *args, **kwargs) 

166 

167 # flux at mag=0 is 10^(zeroPoint/2.5) because m = -2.5*log10(F/F0) 

168 fluxMag0 = 10**(0.4 * self.config.zeroPoint) 

169 self._photoCalib = afwImage.makePhotoCalibFromCalibZeroPoint(fluxMag0, 0.0) 

170 

171 def run(self, exposure, dataRef=None): 

172 """Scale the specified exposure to the desired photometric zeropoint. 

173 

174 Parameters 

175 ---------- 

176 exposure : `lsst.afw.image.Exposure` 

177 Exposure to scale; masked image is scaled in place. 

178 dataRef : `Unknown` 

179 Data reference for exposure. 

180 Not used, but in API so that users can switch between spatially variant 

181 and invariant tasks. 

182 

183 Returns 

184 ------- 

185 result : `lsst.pipe.base.Struct` 

186 Results as a struct with attributes: 

187 

188 ``imageScaler`` 

189 The image scaling object used to scale exposure. 

190 """ 

191 imageScaler = self.computeImageScaler(exposure=exposure, dataRef=dataRef) 

192 mi = exposure.getMaskedImage() 

193 imageScaler.scaleMaskedImage(mi) 

194 return pipeBase.Struct( 

195 imageScaler=imageScaler, 

196 ) 

197 

198 def computeImageScaler(self, exposure, dataRef=None): 

199 """Compute image scaling object for a given exposure. 

200 

201 Parameters 

202 ---------- 

203 exposure : `lsst.afw.image.Exposure` 

204 Exposure for which scaling is desired. 

205 dataRef : `Unknown`, optional 

206 Data reference for exposure. 

207 Not used, but in API so that users can switch between spatially variant 

208 and invariant tasks. 

209 """ 

210 scale = self.scaleFromPhotoCalib(exposure.getPhotoCalib()).scale 

211 return ImageScaler(scale) 

212 

213 def getPhotoCalib(self): 

214 """Get desired PhotoCalib. 

215 

216 Returns 

217 ------- 

218 calibration : `lsst.afw.image.PhotoCalib` 

219 Calibration with ``fluxMag0`` set appropriately for config.zeroPoint. 

220 """ 

221 return self._photoCalib 

222 

223 def scaleFromPhotoCalib(self, calib): 

224 """Compute the scale for the specified PhotoCalib. 

225 

226 Returns 

227 ------- 

228 result : `lsst.pipe.base.Struct` 

229 Results as a struct with attributes: 

230 

231 `scale` 

232 

233 Scale, such that if pixelCalib describes the photometric 

234 zeropoint of a pixel then the following scales that pixel to 

235 the photometric zeropoint specified by config.zeroPoint: 

236 ``scale = computeScale(pixelCalib) pixel *= scale`` 

237 

238 Notes 

239 ----- 

240 Returns a struct to leave room for scaleErr in a future implementation. 

241 """ 

242 fluxAtZeroPoint = calib.magnitudeToInstFlux(self.config.zeroPoint) 

243 return pipeBase.Struct( 

244 scale=1.0 / fluxAtZeroPoint, 

245 ) 

246 

247 def scaleFromFluxMag0(self, fluxMag0): 

248 """Compute the scale for the specified fluxMag0. 

249 

250 This is a wrapper around scaleFromPhotoCalib, which see for more information. 

251 

252 Parameters 

253 ---------- 

254 fluxMag0 : `float` 

255 Flux at magnitude zero. 

256 

257 Returns 

258 ------- 

259 result : `lsst.pipe.base.Struct` 

260 Results as a struct with attributes: 

261 

262 `scale` 

263 

264 Scale, such that if pixelCalib describes the photometric zeropoint 

265 of a pixel then the following scales that pixel to the photometric 

266 zeropoint specified by config.zeroPoint: 

267 ``scale = computeScale(pixelCalib)`` 

268 ``pixel *= scale`` 

269 """ 

270 calib = afwImage.makePhotoCalibFromCalibZeroPoint(fluxMag0, 0.0) 

271 return self.scaleFromPhotoCalib(calib) 

272 

273 

274class SpatialScaleZeroPointTask(ScaleZeroPointTask): 

275 """Compute spatially varying scale factor to scale exposures to a desired photometric zero point. 

276 """ 

277 

278 ConfigClass = SpatialScaleZeroPointConfig 

279 _DefaultName = "scaleZeroPoint" 

280 

281 def __init__(self, *args, **kwargs): 

282 ScaleZeroPointTask.__init__(self, *args, **kwargs) 

283 self.makeSubtask("selectFluxMag0") 

284 

285 def run(self, exposure, dataRef): 

286 """Scale the specified exposure to the desired photometric zeropoint. 

287 

288 Parameters 

289 ---------- 

290 exposure : `lsst.afw.image.Exposure` 

291 Exposure to scale; masked image is scaled in place. 

292 dataRef : `Unknown` 

293 Data reference for exposure. 

294 

295 Returns 

296 ------- 

297 result : `lsst.pipe.base.Struct` 

298 Results as a struct with attributes: 

299 

300 ``imageScaler`` 

301 The image scaling object used to scale exposure. 

302 """ 

303 imageScaler = self.computeImageScaler(exposure=exposure, dataRef=dataRef) 

304 mi = exposure.getMaskedImage() 

305 imageScaler.scaleMaskedImage(mi) 

306 return pipeBase.Struct( 

307 imageScaler=imageScaler, 

308 ) 

309 

310 def computeImageScaler(self, exposure, dataRef): 

311 """Compute image scaling object for a given exposure. 

312 

313 Parameters 

314 ---------- 

315 exposure : `lsst.afw.image.Exposure` 

316 Exposure for which scaling is desired. Only wcs and bbox are used. 

317 dataRef : `Unknown` 

318 Data reference of exposure. 

319 dataRef.dataId used to retrieve all applicable fluxMag0's from a database. 

320 

321 Returns 

322 ------- 

323 result : `SpatialImageScaler` 

324 """ 

325 wcs = exposure.getWcs() 

326 

327 fluxMagInfoList = self.selectFluxMag0.run(dataRef.dataId).fluxMagInfoList 

328 

329 xList = [] 

330 yList = [] 

331 scaleList = [] 

332 

333 for fluxMagInfo in fluxMagInfoList: 

334 # find center of field in tract coordinates 

335 if not fluxMagInfo.coordList: 

336 raise RuntimeError("no x,y data for fluxMagInfo") 

337 ctr = geom.Extent2D() 

338 for coord in fluxMagInfo.coordList: 

339 # accumulate x, y 

340 ctr += geom.Extent2D(wcs.skyToPixel(coord)) 

341 # and find average x, y as the center of the chip 

342 ctr = geom.Point2D(ctr / len(fluxMagInfo.coordList)) 

343 xList.append(ctr.getX()) 

344 yList.append(ctr.getY()) 

345 scaleList.append(self.scaleFromFluxMag0(fluxMagInfo.fluxMag0).scale) 

346 

347 self.log.info("Found %d flux scales for interpolation: %s", 

348 len(scaleList), [f"{s:%0.4f}" for s in scaleList]) 

349 return SpatialImageScaler( 

350 interpStyle=self.config.interpStyle, 

351 xList=xList, 

352 yList=yList, 

353 scaleList=scaleList, 

354 )