Coverage for python/lsst/obs/base/yamlCamera.py: 10%

139 statements  

« prev     ^ index     » next       coverage.py v7.4.2, created at 2024-02-22 11:09 +0000

1# This file is part of obs_base. 

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 

22from functools import lru_cache 

23 

24import lsst.afw.cameraGeom as cameraGeom 

25import lsst.afw.geom as afwGeom 

26import lsst.geom as geom 

27import numpy as np 

28import yaml 

29from lsst.afw.cameraGeom import Amplifier, Camera, ReadoutCorner 

30 

31__all__ = ["makeCamera"] 

32 

33 

34@lru_cache 

35def makeCamera(cameraFile): 

36 """Construct an imaging camera (e.g. the LSST 3Gpix camera). 

37 

38 Parameters 

39 ---------- 

40 cameraFile : `str` 

41 Camera description YAML file. 

42 

43 Returns 

44 ------- 

45 camera : `lsst.afw.cameraGeom.Camera` 

46 The desired Camera 

47 """ 

48 with open(cameraFile) as fd: 

49 cameraParams = yaml.load(fd, Loader=yaml.CLoader) 

50 

51 cameraName = cameraParams["name"] 

52 

53 # 

54 # Handle distortion models. 

55 # 

56 plateScale = geom.Angle(cameraParams["plateScale"], geom.arcseconds) 

57 nativeSys = cameraGeom.CameraSys(cameraParams["transforms"].pop("nativeSys")) 

58 transforms = makeTransformDict(nativeSys, cameraParams["transforms"], plateScale) 

59 

60 ccdParams = cameraParams["CCDs"] 

61 detectorConfigList = makeDetectorConfigList(ccdParams) 

62 

63 amplifierDict = {} 

64 for ccdName, ccdValues in ccdParams.items(): 

65 amplifierDict[ccdName] = makeAmplifierList(ccdValues) 

66 

67 return makeCameraFromCatalogs(cameraName, detectorConfigList, nativeSys, transforms, amplifierDict) 

68 

69 

70def makeDetectorConfigList(ccdParams): 

71 """Make a list of detector configs. 

72 

73 Returns 

74 ------- 

75 detectorConfig : `list` of `lsst.afw.cameraGeom.DetectorConfig` 

76 A list of detector configs. 

77 """ 

78 detectorConfigs = [] 

79 for name, ccd in ccdParams.items(): 

80 detectorConfig = cameraGeom.DetectorConfig() 

81 detectorConfigs.append(detectorConfig) 

82 

83 detectorConfig.name = name 

84 detectorConfig.id = ccd["id"] 

85 detectorConfig.serial = ccd["serial"] 

86 detectorConfig.detectorType = ccd["detectorType"] 

87 if "physicalType" in ccd: 

88 detectorConfig.physicalType = ccd["physicalType"] 

89 # This is the orientation we need to put the serial direction along 

90 # the x-axis 

91 detectorConfig.bbox_x0, detectorConfig.bbox_y0 = ccd["bbox"][0] 

92 detectorConfig.bbox_x1, detectorConfig.bbox_y1 = ccd["bbox"][1] 

93 detectorConfig.pixelSize_x, detectorConfig.pixelSize_y = ccd["pixelSize"] 

94 detectorConfig.transformDict.nativeSys = ccd["transformDict"]["nativeSys"] 

95 transforms = ccd["transformDict"]["transforms"] 

96 detectorConfig.transformDict.transforms = None if transforms == "None" else transforms 

97 detectorConfig.refpos_x, detectorConfig.refpos_y = ccd["refpos"] 

98 if len(ccd["offset"]) == 2: 

99 detectorConfig.offset_x, detectorConfig.offset_y = ccd["offset"] 

100 detectorConfig.offset_z = 0.0 

101 else: 

102 detectorConfig.offset_x, detectorConfig.offset_y, detectorConfig.offset_z = ccd["offset"] 

103 detectorConfig.transposeDetector = ccd["transposeDetector"] 

104 detectorConfig.pitchDeg = ccd["pitch"] 

105 detectorConfig.yawDeg = ccd["yaw"] 

106 detectorConfig.rollDeg = ccd["roll"] 

107 if "crosstalk" in ccd: 

108 detectorConfig.crosstalk = ccd["crosstalk"] 

109 

110 return detectorConfigs 

111 

112 

113def makeAmplifierList(ccd): 

114 """Construct a list of AmplifierBuilder objects.""" 

115 # Much of this will need to be filled in when we know it. 

116 assert len(ccd) > 0 

117 amp = list(ccd["amplifiers"].values())[0] 

118 

119 rawBBox = makeBBoxFromList(amp["rawBBox"]) # total in file 

120 xRawExtent, yRawExtent = rawBBox.getDimensions() 

121 

122 readCorners = { 

123 "LL": ReadoutCorner.LL, 

124 "LR": ReadoutCorner.LR, 

125 "UL": ReadoutCorner.UL, 

126 "UR": ReadoutCorner.UR, 

127 } 

128 

129 amplifierList = [] 

130 for name, amp in sorted(ccd["amplifiers"].items(), key=lambda x: x[1]["hdu"]): 

131 amplifier = Amplifier.Builder() 

132 amplifier.setName(name) 

133 

134 ix, iy = amp["ixy"] 

135 perAmpData = amp["perAmpData"] 

136 if perAmpData: 

137 x0, y0 = 0, 0 # origin of data within each amp image 

138 else: 

139 x0, y0 = ix * xRawExtent, iy * yRawExtent 

140 

141 rawDataBBox = makeBBoxFromList(amp["rawDataBBox"]) # Photosensitive area 

142 xDataExtent, yDataExtent = rawDataBBox.getDimensions() 

143 amplifier.setBBox( 

144 geom.BoxI(geom.PointI(ix * xDataExtent, iy * yDataExtent), rawDataBBox.getDimensions()) 

145 ) 

146 

147 rawBBox = makeBBoxFromList(amp["rawBBox"]) 

148 rawBBox.shift(geom.ExtentI(x0, y0)) 

149 amplifier.setRawBBox(rawBBox) 

150 

151 rawDataBBox = makeBBoxFromList(amp["rawDataBBox"]) 

152 rawDataBBox.shift(geom.ExtentI(x0, y0)) 

153 amplifier.setRawDataBBox(rawDataBBox) 

154 

155 rawSerialOverscanBBox = makeBBoxFromList(amp["rawSerialOverscanBBox"]) 

156 rawSerialOverscanBBox.shift(geom.ExtentI(x0, y0)) 

157 amplifier.setRawHorizontalOverscanBBox(rawSerialOverscanBBox) 

158 

159 rawParallelOverscanBBox = makeBBoxFromList(amp["rawParallelOverscanBBox"]) 

160 rawParallelOverscanBBox.shift(geom.ExtentI(x0, y0)) 

161 amplifier.setRawVerticalOverscanBBox(rawParallelOverscanBBox) 

162 

163 rawSerialPrescanBBox = makeBBoxFromList(amp["rawSerialPrescanBBox"]) 

164 rawSerialPrescanBBox.shift(geom.ExtentI(x0, y0)) 

165 amplifier.setRawPrescanBBox(rawSerialPrescanBBox) 

166 

167 if perAmpData: 

168 amplifier.setRawXYOffset(geom.Extent2I(ix * xRawExtent, iy * yRawExtent)) 

169 else: 

170 amplifier.setRawXYOffset(geom.Extent2I(0, 0)) 

171 

172 amplifier.setReadoutCorner(readCorners[amp["readCorner"]]) 

173 amplifier.setGain(amp["gain"]) 

174 amplifier.setReadNoise(amp["readNoise"]) 

175 amplifier.setSaturation(amp["saturation"]) 

176 amplifier.setSuspectLevel(amp.get("suspect", np.nan)) 

177 

178 # flip data when assembling if needs be (e.g. data from the serial at 

179 # the top of a CCD) 

180 flipX, flipY = amp.get("flipXY") 

181 

182 amplifier.setRawFlipX(flipX) 

183 amplifier.setRawFlipY(flipY) 

184 # linearity placeholder stuff 

185 amplifier.setLinearityCoeffs([float(val) for val in amp["linearityCoeffs"]]) 

186 amplifier.setLinearityType(amp["linearityType"]) 

187 amplifier.setLinearityThreshold(float(amp["linearityThreshold"])) 

188 amplifier.setLinearityMaximum(float(amp["linearityMax"])) 

189 amplifier.setLinearityUnits("DN") 

190 amplifierList.append(amplifier) 

191 return amplifierList 

192 

193 

194def makeAmpInfoCatalog(ccd): 

195 """Backward compatible name.""" 

196 return makeAmplifierList(ccd) 

197 

198 

199def makeBBoxFromList(ylist): 

200 """Given a list [(x0, y0), (xsize, ysize)], probably from a yaml file, 

201 return a BoxI. 

202 """ 

203 (x0, y0), (xsize, ysize) = ylist 

204 return geom.BoxI(geom.PointI(x0, y0), geom.ExtentI(xsize, ysize)) 

205 

206 

207def makeTransformDict(nativeSys, transformDict, plateScale): 

208 """Make a dictionary of TransformPoint2ToPoint2s from yaml, mapping from 

209 nativeSys. 

210 

211 Parameters 

212 ---------- 

213 nativeSys : `lsst.afw.cameraGeom.CameraSys` 

214 transformDict : `dict` 

215 A dict specifying parameters of transforms; keys are camera system 

216 names. 

217 plateScale : `lsst.geom.Angle` 

218 The size of a pixel in angular units/mm (e.g. 20 arcsec/mm for LSST) 

219 

220 Returns 

221 ------- 

222 transforms : `dict` 

223 A dict of `lsst.afw.cameraGeom.CameraSys` : 

224 `lsst.afw.geom.TransformPoint2ToPoint2` 

225 

226 The resulting dict's keys are `~lsst.afw.cameraGeom.CameraSys`, 

227 and the values are Transforms *from* NativeSys *to* CameraSys 

228 """ 

229 # As other comments note this is required, and this is one function where 

230 # it's assumed 

231 assert nativeSys == cameraGeom.FOCAL_PLANE, "Cameras with nativeSys != FOCAL_PLANE are not supported." 

232 

233 resMap = {} 

234 

235 for key, transform in transformDict.items(): 

236 transformType = transform["transformType"] 

237 knownTransformTypes = ["affine", "radial"] 

238 if transformType not in knownTransformTypes: 

239 raise RuntimeError( 

240 "Saw unknown transform type for %s: %s (known types are: [%s])" 

241 % (key, transform["transformType"], ", ".join(knownTransformTypes)) 

242 ) 

243 

244 if transformType == "affine": 

245 affine = geom.AffineTransform(np.array(transform["linear"]), np.array(transform["translation"])) 

246 

247 transform = afwGeom.makeTransform(affine) 

248 elif transformType == "radial": 

249 # radial coefficients of the form 

250 # [0, 1 (no units), C2 (mm^-1), usually 0, C4 (mm^-3), ...] 

251 # Radial distortion is modeled as a radial polynomial that converts 

252 # from focal plane radius (in mm) to field angle (in radians). 

253 # The provided coefficients are divided by the plate 

254 # scale (in radians/mm) meaning that C1 is always 1. 

255 radialCoeffs = np.array(transform["coeffs"]) 

256 

257 radialCoeffs *= plateScale.asRadians() 

258 transform = afwGeom.makeRadialTransform(radialCoeffs) 

259 else: 

260 raise RuntimeError( 

261 'Impossible condition "%s" is not in: [%s])' 

262 % (transform["transformType"], ", ".join(knownTransformTypes)) 

263 ) 

264 

265 resMap[cameraGeom.CameraSys(key)] = transform 

266 

267 return resMap 

268 

269 

270def makeCameraFromCatalogs( 

271 cameraName, 

272 detectorConfigList, 

273 nativeSys, 

274 transformDict, 

275 amplifierDict, 

276 pupilFactoryClass=cameraGeom.pupil.PupilFactory, 

277): 

278 """Construct a Camera instance from a dictionary of 

279 detector name and `lsst.afw.cameraGeom.Amplifier`. 

280 

281 Parameters 

282 ---------- 

283 cameraName : `str` 

284 The name of the camera 

285 detectorConfigList : `list` 

286 A list of `lsst.afw.cameraGeom.cameraConfig.DetectorConfig` 

287 nativeSys : `lsst.afw.cameraGeom.CameraSys` 

288 The native transformation type; must be 

289 `lsst.afw.cameraGeom.FOCAL_PLANE` 

290 transformDict : `dict` 

291 A dict of lsst.afw.cameraGeom.CameraSys : 

292 `lsst.afw.geom.TransformPoint2ToPoint2` 

293 amplifierDict : `dict` [`str`, `lsst.afw.cameraGeom.Amplifier.Builder` ] 

294 A dictionary of detector name and amplifier builders. 

295 pupilFactoryClass : `type` [ `lsst.default afw.cameraGeom.PupilFactory`], \ 

296 optional 

297 Class to attach to camera. 

298 

299 Returns 

300 ------- 

301 camera : `lsst.afw.cameraGeom.Camera` 

302 New Camera instance. 

303 

304 Notes 

305 ----- 

306 Copied from `lsst.afw.cameraGeom.cameraFactory` with permission and 

307 encouragement from Jim Bosch. 

308 """ 

309 # nativeSys=FOCAL_PLANE seems to be assumed in various places in this file 

310 # (e.g. the definition of TAN_PIXELS), despite CameraConfig providing the 

311 # illusion that it's configurable. 

312 # Note that we can't actually get rid of the nativeSys config option 

313 # without breaking lots of on-disk camera configs. 

314 assert nativeSys == cameraGeom.FOCAL_PLANE, "Cameras with nativeSys != FOCAL_PLANE are not supported." 

315 

316 focalPlaneToField = transformDict[cameraGeom.FIELD_ANGLE] 

317 

318 cameraBuilder = Camera.Builder(cameraName) 

319 cameraBuilder.setPupilFactoryClass(pupilFactoryClass) 

320 

321 # Ensure all transforms in the camera transform dict are included. 

322 for toSys, transform in transformDict.items(): 

323 cameraBuilder.setTransformFromFocalPlaneTo(toSys, transform) 

324 

325 for detectorConfig in detectorConfigList: 

326 # This should build all detector pixel -> focalPlane transforms. 

327 cameraGeom.addDetectorBuilderFromConfig( 

328 cameraBuilder, detectorConfig, amplifierDict[detectorConfig.name], focalPlaneToField 

329 ) 

330 

331 # For reasons I don't understand, some obs_ packages (e.g. HSC) set 

332 # nativeSys to None for their detectors (which doesn't seem to be 

333 # permitted by the config class!), but they really mean PIXELS. For 

334 # backwards compatibility we use that as the default... 

335 detectorNativeSys = detectorConfig.transformDict.nativeSys 

336 detectorNativeSys = ( 

337 cameraGeom.PIXELS if detectorNativeSys is None else cameraGeom.CameraSysPrefix(detectorNativeSys) 

338 ) 

339 

340 # ...well, actually, it seems that we've always assumed down in C++ 

341 # that the answer is always PIXELS without ever checking that it is. 

342 # So let's assert that it is, since there are hints all over this file 

343 # (e.g. the definition of TAN_PIXELS) that other parts of the codebase 

344 # have regularly made that assumption as well. Note that we can't 

345 # actually get rid of the nativeSys config option without breaking 

346 # lots of on-disk camera configs. 

347 assert detectorNativeSys == cameraGeom.PIXELS, "Detectors with nativeSys != PIXELS are not supported." 

348 detectorNativeSys = cameraGeom.CameraSys(detectorNativeSys, detectorConfig.name) 

349 

350 return cameraBuilder.finish()