Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 

22import yaml 

23 

24from functools import lru_cache 

25import numpy as np 

26import lsst.afw.cameraGeom as cameraGeom 

27import lsst.geom as geom 

28import lsst.afw.geom as afwGeom 

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

30 

31 

32__all__ = ["makeCamera"] 

33 

34 

35@lru_cache() 

36def makeCamera(cameraFile): 

37 """An imaging camera (e.g. the LSST 3Gpix camera) 

38 

39 Parameters 

40 ---------- 

41 cameraFile : `str` 

42 Camera description YAML file. 

43 

44 Returns 

45 ------- 

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

47 The desired Camera 

48 """ 

49 

50 with open(cameraFile) as fd: 

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

52 

53 cameraName = cameraParams["name"] 

54 

55 # 

56 # Handle distortion models. 

57 # 

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

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

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

61 

62 ccdParams = cameraParams["CCDs"] 

63 detectorConfigList = makeDetectorConfigList(ccdParams) 

64 

65 amplifierDict = {} 

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

67 amplifierDict[ccdName] = makeAmplifierList(ccdValues) 

68 

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

70 

71 

72def makeDetectorConfigList(ccdParams): 

73 """Make a list of detector configs 

74 

75 Returns 

76 ------- 

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

78 A list of detector configs. 

79 """ 

80 detectorConfigs = [] 

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

82 detectorConfig = cameraGeom.DetectorConfig() 

83 detectorConfigs.append(detectorConfig) 

84 

85 detectorConfig.name = name 

86 detectorConfig.id = ccd['id'] 

87 detectorConfig.serial = ccd['serial'] 

88 detectorConfig.detectorType = ccd['detectorType'] 

89 if 'physicalType' in ccd: 

90 detectorConfig.physicalType = ccd['physicalType'] 

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

92 # the x-axis 

93 detectorConfig.bbox_x0, detectorConfig.bbox_y0 = ccd['bbox'][0] 

94 detectorConfig.bbox_x1, detectorConfig.bbox_y1 = ccd['bbox'][1] 

95 detectorConfig.pixelSize_x, detectorConfig.pixelSize_y = ccd['pixelSize'] 

96 detectorConfig.transformDict.nativeSys = ccd['transformDict']['nativeSys'] 

97 transforms = ccd['transformDict']['transforms'] 

98 detectorConfig.transformDict.transforms = None if transforms == 'None' else transforms 

99 detectorConfig.refpos_x, detectorConfig.refpos_y = ccd['refpos'] 

100 detectorConfig.offset_x, detectorConfig.offset_y = ccd['offset'] 

101 detectorConfig.transposeDetector = ccd['transposeDetector'] 

102 detectorConfig.pitchDeg = ccd['pitch'] 

103 detectorConfig.yawDeg = ccd['yaw'] 

104 detectorConfig.rollDeg = ccd['roll'] 

105 if 'crosstalk' in ccd: 

106 detectorConfig.crosstalk = ccd['crosstalk'] 

107 

108 return detectorConfigs 

109 

110 

111def makeAmplifierList(ccd): 

112 """Construct a list of AmplifierBuilder objects 

113 """ 

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

115 assert len(ccd) > 0 

116 amp = list(ccd['amplifiers'].values())[0] 

117 

118 rawBBox = makeBBoxFromList(amp['rawBBox']) # total in file 

119 xRawExtent, yRawExtent = rawBBox.getDimensions() 

120 

121 readCorners = {"LL": ReadoutCorner.LL, 

122 "LR": ReadoutCorner.LR, 

123 "UL": ReadoutCorner.UL, 

124 "UR": ReadoutCorner.UR} 

125 

126 amplifierList = [] 

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

128 amplifier = Amplifier.Builder() 

129 amplifier.setName(name) 

130 

131 ix, iy = amp['ixy'] 

132 perAmpData = amp['perAmpData'] 

133 if perAmpData: 

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

135 else: 

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

137 

138 rawDataBBox = makeBBoxFromList(amp['rawDataBBox']) # Photosensitive area 

139 xDataExtent, yDataExtent = rawDataBBox.getDimensions() 

140 amplifier.setBBox(geom.BoxI( 

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

142 

143 rawBBox = makeBBoxFromList(amp['rawBBox']) 

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

145 amplifier.setRawBBox(rawBBox) 

146 

147 rawDataBBox = makeBBoxFromList(amp['rawDataBBox']) 

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

149 amplifier.setRawDataBBox(rawDataBBox) 

150 

151 rawSerialOverscanBBox = makeBBoxFromList(amp['rawSerialOverscanBBox']) 

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

153 amplifier.setRawHorizontalOverscanBBox(rawSerialOverscanBBox) 

154 

155 rawParallelOverscanBBox = makeBBoxFromList(amp['rawParallelOverscanBBox']) 

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

157 amplifier.setRawVerticalOverscanBBox(rawParallelOverscanBBox) 

158 

159 rawSerialPrescanBBox = makeBBoxFromList(amp['rawSerialPrescanBBox']) 

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

161 amplifier.setRawPrescanBBox(rawSerialPrescanBBox) 

162 

163 if perAmpData: 

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

165 else: 

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

167 

168 amplifier.setReadoutCorner(readCorners[amp['readCorner']]) 

169 amplifier.setGain(amp['gain']) 

170 amplifier.setReadNoise(amp['readNoise']) 

171 amplifier.setSaturation(amp['saturation']) 

172 amplifier.setSuspectLevel(amp.get('suspect', np.nan)) 

173 

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

175 # the top of a CCD) 

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

177 

178 amplifier.setRawFlipX(flipX) 

179 amplifier.setRawFlipY(flipY) 

180 # linearity placeholder stuff 

181 amplifier.setLinearityCoeffs([float(val) for val in amp['linearityCoeffs']]) 

182 amplifier.setLinearityType(amp['linearityType']) 

183 amplifier.setLinearityThreshold(float(amp['linearityThreshold'])) 

184 amplifier.setLinearityMaximum(float(amp['linearityMax'])) 

185 amplifier.setLinearityUnits("DN") 

186 amplifierList.append(amplifier) 

187 return amplifierList 

188 

189 

190def makeAmpInfoCatalog(ccd): 

191 """Backward compatible name. 

192 """ 

193 return makeAmplifierList(ccd) 

194 

195 

196def makeBBoxFromList(ylist): 

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

198 return a BoxI 

199 """ 

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

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

202 

203 

204def makeTransformDict(nativeSys, transformDict, plateScale): 

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

206 nativeSys 

207 

208 Parameters 

209 ---------- 

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

211 transformDict : `dict` 

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

213 names. 

214 plateScale : `lsst.geom.Angle` 

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

216 

217 Returns 

218 ------- 

219 transforms : `dict` 

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

221 `lsst.afw.geom.TransformPoint2ToPoint2` 

222 

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

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

225 """ 

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

227 # it's assumed 

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

229 

230 resMap = dict() 

231 

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

233 transformType = transform["transformType"] 

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

235 if transformType not in knownTransformTypes: 

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

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

238 

239 if transformType == "affine": 

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

241 np.array(transform["translation"])) 

242 

243 transform = afwGeom.makeTransform(affine) 

244 elif transformType == "radial": 

245 # radial coefficients of the form [0, 1 (no units), C2 (rad), 

246 # usually 0, C3 (rad^2), ...] 

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

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

249 # The provided coefficients are divided by the plate 

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

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

252 

253 radialCoeffs *= plateScale.asRadians() 

254 transform = afwGeom.makeRadialTransform(radialCoeffs) 

255 else: 

256 raise RuntimeError("Impossible condition \"%s\" is not in: [%s])" % ( 

257 transform["transformType"], ", ".join(knownTransformTypes))) 

258 

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

260 

261 return resMap 

262 

263 

264def makeCameraFromCatalogs(cameraName, detectorConfigList, nativeSys, transformDict, amplifierDict, 

265 pupilFactoryClass=cameraGeom.pupil.PupilFactory): 

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

267 detector name : `lsst.afw.cameraGeom.amplifier` 

268 

269 Parameters 

270 ---------- 

271 cameraName : `str` 

272 The name of the camera 

273 detectorConfigList : `list` 

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

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

276 The native transformation type; must be 

277 `lsst.afw.cameraGeom.FOCAL_PLANE` 

278 transformDict : `dict` 

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

280 `lsst.afw.geom.TransformPoint2ToPoint2` 

281 amplifierDict : `dict` 

282 A dictionary of detector name : 

283 `lsst.afw.cameraGeom.Amplifier.Builder` 

284 pupilFactoryClass : `type`, optional 

285 Class to attach to camera; 

286 `lsst.default afw.cameraGeom.PupilFactory` 

287 

288 Returns 

289 ------- 

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

291 New Camera instance. 

292 

293 Notes 

294 ------ 

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

296 encouragement from Jim Bosch. 

297 """ 

298 

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

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

301 # illusion that it's configurable. 

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

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

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

305 

306 focalPlaneToField = transformDict[cameraGeom.FIELD_ANGLE] 

307 

308 cameraBuilder = Camera.Builder(cameraName) 

309 cameraBuilder.setPupilFactoryClass(pupilFactoryClass) 

310 

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

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

313 cameraBuilder.setTransformFromFocalPlaneTo(toSys, transform) 

314 

315 for detectorConfig in detectorConfigList: 

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

317 cameraGeom.addDetectorBuilderFromConfig(cameraBuilder, detectorConfig, 

318 amplifierDict[detectorConfig.name], 

319 focalPlaneToField) 

320 

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

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

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

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

325 detectorNativeSys = detectorConfig.transformDict.nativeSys 

326 detectorNativeSys = (cameraGeom.PIXELS if detectorNativeSys is None else 

327 cameraGeom.CameraSysPrefix(detectorNativeSys)) 

328 

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

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

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

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

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

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

335 # lots of on-disk camera configs. 

336 assert detectorNativeSys == cameraGeom.PIXELS, \ 

337 "Detectors with nativeSys != PIXELS are not supported." 

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

339 

340 return cameraBuilder.finish()