Coverage for python/lsst/pipe/tasks/mocks/mockObservation.py: 95%

116 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-12-01 21:10 +0000

1# 

2# LSST Data Management System 

3# Copyright 2008, 2009, 2010, 2011, 2012 LSST Corporation. 

4# 

5# This product includes software developed by the 

6# LSST Project (http://www.lsst.org/). 

7# 

8# This program is free software: you can redistribute it and/or modify 

9# it under the terms of the GNU General Public License as published by 

10# the Free Software Foundation, either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# This program is distributed in the hope that it will be useful, 

14# but WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <http://www.lsstcorp.org/LegalNotices/>. 

21# 

22import numpy as np 

23 

24import lsst.pex.config 

25import lsst.geom 

26import lsst.afw.table 

27import lsst.afw.geom 

28from lsst.afw.cameraGeom import PIXELS, FOCAL_PLANE 

29import lsst.afw.image 

30import lsst.afw.math 

31import lsst.afw.detection 

32import lsst.pipe.base 

33from lsst.meas.base.apCorrRegistry import getApCorrNameSet 

34from lsst.meas.algorithms.testUtils import makeRandomTransmissionCurve 

35 

36 

37class MockObservationConfig(lsst.pex.config.Config): 

38 pixelScale = lsst.pex.config.Field( 

39 dtype=float, default=0.2, optional=False, 

40 doc="Pixel scale for mock WCSs in arcseconds/pixel" 

41 ) 

42 doRotate = lsst.pex.config.Field( 

43 dtype=bool, default=True, optional=False, 

44 doc="Whether to randomly rotate observations relative to the tract Wcs" 

45 ) 

46 fluxMag0 = lsst.pex.config.Field( 

47 dtype=float, default=1E11, optional=False, 

48 doc="Flux at zero magnitude used to define PhotoCalibs." 

49 ) 

50 fluxMag0Err = lsst.pex.config.Field( 

51 dtype=float, default=100.0, optional=False, 

52 doc="Error on flux at zero magnitude used to define PhotoCalibs; used to add scatter as well." 

53 ) 

54 expTime = lsst.pex.config.Field( 

55 dtype=float, default=60.0, optional=False, 

56 doc="Exposure time set in visitInfo (does not affect flux or noise level)" 

57 ) 

58 psfImageSize = lsst.pex.config.Field( 

59 dtype=int, default=21, optional=False, 

60 doc="Image width and height of generated Psfs." 

61 ) 

62 psfMinSigma = lsst.pex.config.Field( 

63 dtype=float, default=1.5, optional=False, 

64 doc="Minimum radius for generated Psfs." 

65 ) 

66 psfMaxSigma = lsst.pex.config.Field( 

67 dtype=float, default=3.0, optional=False, 

68 doc="Maximum radius for generated Psfs." 

69 ) 

70 apCorrOrder = lsst.pex.config.Field( 

71 dtype=int, default=1, optional=False, 

72 doc="Polynomial order for aperture correction fields" 

73 ) 

74 seed = lsst.pex.config.Field(dtype=int, default=1, doc="Seed for numpy random number generator") 

75 

76 

77class MockObservationTask(lsst.pipe.base.Task): 

78 """Task to generate mock Exposure parameters (Wcs, Psf, PhotoCalib), intended for use as a subtask 

79 of MockCoaddTask. 

80 

81 @todo: 

82 - document "pa" in detail; angle of what to what? 

83 - document the catalog parameter of the run method 

84 """ 

85 

86 ConfigClass = MockObservationConfig 

87 

88 def __init__(self, **kwds): 

89 lsst.pipe.base.Task.__init__(self, **kwds) 

90 self.schema = lsst.afw.table.ExposureTable.makeMinimalSchema() 

91 self.ccdKey = self.schema.addField("ccd", type=np.int32, doc="CCD number") 

92 self.visitKey = self.schema.addField("visit", type=np.int32, doc="visit number") 

93 self.pointingKey = lsst.afw.table.CoordKey.addFields(self.schema, "pointing", "center of visit") 

94 self.filterKey = self.schema.addField("filter", type=str, doc="Bandpass filter name", size=16) 

95 self.rng = np.random.RandomState(self.config.seed) 

96 

97 def run(self, butler, n, tractInfo, camera, catalog=None): 

98 """Driver that generates an ExposureCatalog of mock observations. 

99 

100 @param[in] butler: a data butler 

101 @param[in] n: number of pointings 

102 @param[in] camera: camera geometry (an lsst.afw.cameraGeom.Camera) 

103 @param[in] catalog: catalog to which to add observations (an ExposureCatalog); 

104 if None then a new catalog is created. 

105 

106 @todo figure out what `pa` is and use that knowledge to set `boresightRotAng` and `rotType` 

107 """ 

108 if catalog is None: 108 ↛ 111line 108 didn't jump to line 111, because the condition on line 108 was never false

109 catalog = lsst.afw.table.ExposureCatalog(self.schema) 

110 else: 

111 if not catalog.getSchema().contains(self.schema): 

112 raise ValueError("Catalog schema does not match Task schema") 

113 visit = 1 

114 

115 for position, pa in self.makePointings(n, tractInfo): 

116 visitInfo = lsst.afw.image.VisitInfo( 

117 exposureTime=self.config.expTime, 

118 date=lsst.daf.base.DateTime.now(), 

119 boresightRaDec=position, 

120 ) 

121 for detector in camera: 

122 photoCalib = self.buildPhotoCalib() 

123 record = catalog.addNew() 

124 record.setI(self.ccdKey, detector.getId()) 

125 record.setI(self.visitKey, visit) 

126 record.set(self.filterKey, 'r') 

127 record.set(self.pointingKey, position) 

128 record.setWcs(self.buildWcs(position, pa, detector)) 

129 record.setPhotoCalib(photoCalib) 

130 record.setVisitInfo(visitInfo) 

131 record.setPsf(self.buildPsf(detector)) 

132 record.setApCorrMap(self.buildApCorrMap(detector)) 

133 record.setTransmissionCurve(self.buildTransmissionCurve(detector)) 

134 record.setBBox(detector.getBBox()) 

135 detectorId = detector.getId() 

136 obj = butler.get("ccdExposureId", visit=visit, ccd=detectorId, immediate=True) 

137 record.setId(obj) 

138 visit += 1 

139 return catalog 

140 

141 def makePointings(self, n, tractInfo): 

142 """Generate (celestial) positions and rotation angles that define field locations. 

143 

144 Default implementation draws random pointings that are uniform in the tract's image 

145 coordinate system. 

146 

147 @param[in] n: number of pointings 

148 @param[in] tractInfo: skymap tract (a lsst.skymap.TractInfo) 

149 @return a Python iterable over (coord, angle) pairs: 

150 - coord is an ICRS object position (an lsst.geom.SpherePoint) 

151 - angle is a position angle (???) (an lsst.geom.Angle) 

152 

153 The default implementation returns an iterator (i.e. the function is a "generator"), 

154 but derived-class overrides may return any iterable. 

155 """ 

156 wcs = tractInfo.getWcs() 

157 bbox = lsst.geom.Box2D(tractInfo.getBBox()) 

158 bbox.grow(lsst.geom.Extent2D(-0.1 * bbox.getWidth(), -0.1 * bbox.getHeight())) 

159 for i in range(n): 

160 x = self.rng.rand() * bbox.getWidth() + bbox.getMinX() 

161 y = self.rng.rand() * bbox.getHeight() + bbox.getMinY() 

162 pa = 0.0 * lsst.geom.radians 

163 if self.config.doRotate: 163 ↛ 165line 163 didn't jump to line 165, because the condition on line 163 was never false

164 pa = self.rng.rand() * 2.0 * np.pi * lsst.geom.radians 

165 yield wcs.pixelToSky(x, y), pa 

166 

167 def buildWcs(self, position, pa, detector): 

168 """Build a simple TAN Wcs with no distortion and exactly-aligned CCDs. 

169 

170 @param[in] position: ICRS object position on sky (on lsst.geom.SpherePoint) 

171 @param[in] pa: position angle (an lsst.geom.Angle) 

172 @param[in] detector: detector information (an lsst.afw.cameraGeom.Detector) 

173 """ 

174 crval = position 

175 pixelScale = (self.config.pixelScale * lsst.geom.arcseconds).asDegrees() 

176 cd = (lsst.geom.LinearTransform.makeScaling(pixelScale) 

177 * lsst.geom.LinearTransform.makeRotation(pa)) 

178 crpix = detector.transform(lsst.geom.Point2D(0, 0), FOCAL_PLANE, PIXELS) 

179 wcs = lsst.afw.geom.makeSkyWcs(crpix=crpix, crval=crval, cdMatrix=cd.getMatrix()) 

180 return wcs 

181 

182 def buildPhotoCalib(self): 

183 """Build a simple PhotoCalib object with the calibration factor 

184 drawn from a Gaussian defined by config. 

185 """ 

186 photoCalib = lsst.afw.image.makePhotoCalibFromCalibZeroPoint( 

187 self.rng.randn() * self.config.fluxMag0Err + self.config.fluxMag0, 

188 self.config.fluxMag0Err 

189 ) 

190 return photoCalib 

191 

192 def buildPsf(self, detector): 

193 """Build a simple Gaussian Psf with linearly-varying ellipticity and size. 

194 

195 The Psf pattern increases sigma_x linearly along the x direction, and sigma_y 

196 linearly along the y direction. 

197 

198 @param[in] detector: detector information (an lsst.afw.cameraGeom.Detector) 

199 @return a psf (an instance of lsst.meas.algorithms.KernelPsf) 

200 """ 

201 bbox = detector.getBBox() 

202 dx = (self.config.psfMaxSigma - self.config.psfMinSigma) / bbox.getWidth() 

203 dy = (self.config.psfMaxSigma - self.config.psfMinSigma) / bbox.getHeight() 

204 sigmaXFunc = lsst.afw.math.PolynomialFunction2D(1) 

205 sigmaXFunc.setParameter(0, self.config.psfMinSigma - dx * bbox.getMinX() - dy * bbox.getMinY()) 

206 sigmaXFunc.setParameter(1, dx) 

207 sigmaXFunc.setParameter(2, 0.0) 

208 sigmaYFunc = lsst.afw.math.PolynomialFunction2D(1) 

209 sigmaYFunc.setParameter(0, self.config.psfMinSigma) 

210 sigmaYFunc.setParameter(1, 0.0) 

211 sigmaYFunc.setParameter(2, dy) 

212 angleFunc = lsst.afw.math.PolynomialFunction2D(0) 

213 spatialFuncList = [] 

214 spatialFuncList.append(sigmaXFunc) 

215 spatialFuncList.append(sigmaYFunc) 

216 spatialFuncList.append(angleFunc) 

217 kernel = lsst.afw.math.AnalyticKernel( 

218 self.config.psfImageSize, self.config.psfImageSize, 

219 lsst.afw.math.GaussianFunction2D(self.config.psfMinSigma, self.config.psfMinSigma), 

220 spatialFuncList 

221 ) 

222 return lsst.meas.algorithms.KernelPsf(kernel) 

223 

224 def buildApCorrMap(self, detector): 

225 """Build an ApCorrMap with random linearly-varying fields for all 

226 flux fields registered for aperture correction. 

227 

228 These flux field names are used only as strings; there is no 

229 connection to any actual algorithms with those names or the PSF model. 

230 """ 

231 order = self.config.apCorrOrder 

232 

233 def makeRandomBoundedField(): 

234 """Make an upper-left triangular coefficient array appropriate 

235 for a 2-d polynomial.""" 

236 array = np.zeros((order + 1, order + 1), dtype=float) 

237 for n in range(order + 1): 

238 array[n, 0:order + 1 - n] = self.rng.randn(order + 1 - n) 

239 return lsst.afw.math.ChebyshevBoundedField(bbox, array) 

240 

241 bbox = detector.getBBox() 

242 apCorrMap = lsst.afw.image.ApCorrMap() 

243 for name in getApCorrNameSet(): 

244 apCorrMap.set(name + "_instFlux", makeRandomBoundedField()) 

245 apCorrMap.set(name + "_instFluxErr", makeRandomBoundedField()) 

246 return apCorrMap 

247 

248 def buildTransmissionCurve(self, detector): 

249 """Build a random spacially-varying TransmissionCurve.""" 

250 bbox = detector.getBBox() 

251 return makeRandomTransmissionCurve(rng=self.rng, maxRadius=max(bbox.getWidth(), bbox.getHeight()))