Coverage for python/lsst/obs/base/yamlCamera.py: 10%
136 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-16 02:16 -0700
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-16 02:16 -0700
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/>.
22from functools import lru_cache
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
31__all__ = ["makeCamera"]
34@lru_cache()
35def makeCamera(cameraFile):
36 """An imaging camera (e.g. the LSST 3Gpix camera)
38 Parameters
39 ----------
40 cameraFile : `str`
41 Camera description YAML file.
43 Returns
44 -------
45 camera : `lsst.afw.cameraGeom.Camera`
46 The desired Camera
47 """
49 with open(cameraFile) as fd:
50 cameraParams = yaml.load(fd, Loader=yaml.CLoader)
52 cameraName = cameraParams["name"]
54 #
55 # Handle distortion models.
56 #
57 plateScale = geom.Angle(cameraParams["plateScale"], geom.arcseconds)
58 nativeSys = cameraGeom.CameraSys(cameraParams["transforms"].pop("nativeSys"))
59 transforms = makeTransformDict(nativeSys, cameraParams["transforms"], plateScale)
61 ccdParams = cameraParams["CCDs"]
62 detectorConfigList = makeDetectorConfigList(ccdParams)
64 amplifierDict = {}
65 for ccdName, ccdValues in ccdParams.items():
66 amplifierDict[ccdName] = makeAmplifierList(ccdValues)
68 return makeCameraFromCatalogs(cameraName, detectorConfigList, nativeSys, transforms, amplifierDict)
71def makeDetectorConfigList(ccdParams):
72 """Make a list of detector configs
74 Returns
75 -------
76 detectorConfig : `list` of `lsst.afw.cameraGeom.DetectorConfig`
77 A list of detector configs.
78 """
79 detectorConfigs = []
80 for name, ccd in ccdParams.items():
81 detectorConfig = cameraGeom.DetectorConfig()
82 detectorConfigs.append(detectorConfig)
84 detectorConfig.name = name
85 detectorConfig.id = ccd["id"]
86 detectorConfig.serial = ccd["serial"]
87 detectorConfig.detectorType = ccd["detectorType"]
88 if "physicalType" in ccd:
89 detectorConfig.physicalType = ccd["physicalType"]
90 # This is the orientation we need to put the serial direction along
91 # the x-axis
92 detectorConfig.bbox_x0, detectorConfig.bbox_y0 = ccd["bbox"][0]
93 detectorConfig.bbox_x1, detectorConfig.bbox_y1 = ccd["bbox"][1]
94 detectorConfig.pixelSize_x, detectorConfig.pixelSize_y = ccd["pixelSize"]
95 detectorConfig.transformDict.nativeSys = ccd["transformDict"]["nativeSys"]
96 transforms = ccd["transformDict"]["transforms"]
97 detectorConfig.transformDict.transforms = None if transforms == "None" else transforms
98 detectorConfig.refpos_x, detectorConfig.refpos_y = ccd["refpos"]
99 detectorConfig.offset_x, detectorConfig.offset_y = ccd["offset"]
100 detectorConfig.transposeDetector = ccd["transposeDetector"]
101 detectorConfig.pitchDeg = ccd["pitch"]
102 detectorConfig.yawDeg = ccd["yaw"]
103 detectorConfig.rollDeg = ccd["roll"]
104 if "crosstalk" in ccd:
105 detectorConfig.crosstalk = ccd["crosstalk"]
107 return detectorConfigs
110def makeAmplifierList(ccd):
111 """Construct a list of AmplifierBuilder objects"""
112 # Much of this will need to be filled in when we know it.
113 assert len(ccd) > 0
114 amp = list(ccd["amplifiers"].values())[0]
116 rawBBox = makeBBoxFromList(amp["rawBBox"]) # total in file
117 xRawExtent, yRawExtent = rawBBox.getDimensions()
119 readCorners = {
120 "LL": ReadoutCorner.LL,
121 "LR": ReadoutCorner.LR,
122 "UL": ReadoutCorner.UL,
123 "UR": ReadoutCorner.UR,
124 }
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)
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
138 rawDataBBox = makeBBoxFromList(amp["rawDataBBox"]) # Photosensitive area
139 xDataExtent, yDataExtent = rawDataBBox.getDimensions()
140 amplifier.setBBox(
141 geom.BoxI(geom.PointI(ix * xDataExtent, iy * yDataExtent), rawDataBBox.getDimensions())
142 )
144 rawBBox = makeBBoxFromList(amp["rawBBox"])
145 rawBBox.shift(geom.ExtentI(x0, y0))
146 amplifier.setRawBBox(rawBBox)
148 rawDataBBox = makeBBoxFromList(amp["rawDataBBox"])
149 rawDataBBox.shift(geom.ExtentI(x0, y0))
150 amplifier.setRawDataBBox(rawDataBBox)
152 rawSerialOverscanBBox = makeBBoxFromList(amp["rawSerialOverscanBBox"])
153 rawSerialOverscanBBox.shift(geom.ExtentI(x0, y0))
154 amplifier.setRawHorizontalOverscanBBox(rawSerialOverscanBBox)
156 rawParallelOverscanBBox = makeBBoxFromList(amp["rawParallelOverscanBBox"])
157 rawParallelOverscanBBox.shift(geom.ExtentI(x0, y0))
158 amplifier.setRawVerticalOverscanBBox(rawParallelOverscanBBox)
160 rawSerialPrescanBBox = makeBBoxFromList(amp["rawSerialPrescanBBox"])
161 rawSerialPrescanBBox.shift(geom.ExtentI(x0, y0))
162 amplifier.setRawPrescanBBox(rawSerialPrescanBBox)
164 if perAmpData:
165 amplifier.setRawXYOffset(geom.Extent2I(ix * xRawExtent, iy * yRawExtent))
166 else:
167 amplifier.setRawXYOffset(geom.Extent2I(0, 0))
169 amplifier.setReadoutCorner(readCorners[amp["readCorner"]])
170 amplifier.setGain(amp["gain"])
171 amplifier.setReadNoise(amp["readNoise"])
172 amplifier.setSaturation(amp["saturation"])
173 amplifier.setSuspectLevel(amp.get("suspect", np.nan))
175 # flip data when assembling if needs be (e.g. data from the serial at
176 # the top of a CCD)
177 flipX, flipY = amp.get("flipXY")
179 amplifier.setRawFlipX(flipX)
180 amplifier.setRawFlipY(flipY)
181 # linearity placeholder stuff
182 amplifier.setLinearityCoeffs([float(val) for val in amp["linearityCoeffs"]])
183 amplifier.setLinearityType(amp["linearityType"])
184 amplifier.setLinearityThreshold(float(amp["linearityThreshold"]))
185 amplifier.setLinearityMaximum(float(amp["linearityMax"]))
186 amplifier.setLinearityUnits("DN")
187 amplifierList.append(amplifier)
188 return amplifierList
191def makeAmpInfoCatalog(ccd):
192 """Backward compatible name."""
193 return makeAmplifierList(ccd)
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))
204def makeTransformDict(nativeSys, transformDict, plateScale):
205 """Make a dictionary of TransformPoint2ToPoint2s from yaml, mapping from
206 nativeSys
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)
217 Returns
218 -------
219 transforms : `dict`
220 A dict of `lsst.afw.cameraGeom.CameraSys` :
221 `lsst.afw.geom.TransformPoint2ToPoint2`
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."
230 resMap = dict()
232 for key, transform in transformDict.items():
233 transformType = transform["transformType"]
234 knownTransformTypes = ["affine", "radial"]
235 if transformType not in knownTransformTypes:
236 raise RuntimeError(
237 "Saw unknown transform type for %s: %s (known types are: [%s])"
238 % (key, transform["transformType"], ", ".join(knownTransformTypes))
239 )
241 if transformType == "affine":
242 affine = geom.AffineTransform(np.array(transform["linear"]), np.array(transform["translation"]))
244 transform = afwGeom.makeTransform(affine)
245 elif transformType == "radial":
246 # radial coefficients of the form [0, 1 (no units), C2 (rad),
247 # usually 0, C3 (rad^2), ...]
248 # Radial distortion is modeled as a radial polynomial that converts
249 # from focal plane radius (in mm) to field angle (in radians).
250 # The provided coefficients are divided by the plate
251 # scale (in radians/mm) meaning that C1 is always 1.
252 radialCoeffs = np.array(transform["coeffs"])
254 radialCoeffs *= plateScale.asRadians()
255 transform = afwGeom.makeRadialTransform(radialCoeffs)
256 else:
257 raise RuntimeError(
258 'Impossible condition "%s" is not in: [%s])'
259 % (transform["transformType"], ", ".join(knownTransformTypes))
260 )
262 resMap[cameraGeom.CameraSys(key)] = transform
264 return resMap
267def makeCameraFromCatalogs(
268 cameraName,
269 detectorConfigList,
270 nativeSys,
271 transformDict,
272 amplifierDict,
273 pupilFactoryClass=cameraGeom.pupil.PupilFactory,
274):
275 """Construct a Camera instance from a dictionary of
276 detector name : `lsst.afw.cameraGeom.amplifier`
278 Parameters
279 ----------
280 cameraName : `str`
281 The name of the camera
282 detectorConfigList : `list`
283 A list of `lsst.afw.cameraGeom.cameraConfig.DetectorConfig`
284 nativeSys : `lsst.afw.cameraGeom.CameraSys`
285 The native transformation type; must be
286 `lsst.afw.cameraGeom.FOCAL_PLANE`
287 transformDict : `dict`
288 A dict of lsst.afw.cameraGeom.CameraSys :
289 `lsst.afw.geom.TransformPoint2ToPoint2`
290 amplifierDict : `dict`
291 A dictionary of detector name :
292 `lsst.afw.cameraGeom.Amplifier.Builder`
293 pupilFactoryClass : `type`, optional
294 Class to attach to camera;
295 `lsst.default afw.cameraGeom.PupilFactory`
297 Returns
298 -------
299 camera : `lsst.afw.cameraGeom.Camera`
300 New Camera instance.
302 Notes
303 ------
304 Copied from `lsst.afw.cameraGeom.cameraFactory` with permission and
305 encouragement from Jim Bosch.
306 """
308 # nativeSys=FOCAL_PLANE seems to be assumed in various places in this file
309 # (e.g. the definition of TAN_PIXELS), despite CameraConfig providing the
310 # illusion that it's configurable.
311 # Note that we can't actually get rid of the nativeSys config option
312 # without breaking lots of on-disk camera configs.
313 assert nativeSys == cameraGeom.FOCAL_PLANE, "Cameras with nativeSys != FOCAL_PLANE are not supported."
315 focalPlaneToField = transformDict[cameraGeom.FIELD_ANGLE]
317 cameraBuilder = Camera.Builder(cameraName)
318 cameraBuilder.setPupilFactoryClass(pupilFactoryClass)
320 # Ensure all transforms in the camera transform dict are included.
321 for toSys, transform in transformDict.items():
322 cameraBuilder.setTransformFromFocalPlaneTo(toSys, transform)
324 for detectorConfig in detectorConfigList:
325 # This should build all detector pixel -> focalPlane transforms.
326 cameraGeom.addDetectorBuilderFromConfig(
327 cameraBuilder, detectorConfig, amplifierDict[detectorConfig.name], focalPlaneToField
328 )
330 # For reasons I don't understand, some obs_ packages (e.g. HSC) set
331 # nativeSys to None for their detectors (which doesn't seem to be
332 # permitted by the config class!), but they really mean PIXELS. For
333 # backwards compatibility we use that as the default...
334 detectorNativeSys = detectorConfig.transformDict.nativeSys
335 detectorNativeSys = (
336 cameraGeom.PIXELS if detectorNativeSys is None else cameraGeom.CameraSysPrefix(detectorNativeSys)
337 )
339 # ...well, actually, it seems that we've always assumed down in C++
340 # that the answer is always PIXELS without ever checking that it is.
341 # So let's assert that it is, since there are hints all over this file
342 # (e.g. the definition of TAN_PIXELS) that other parts of the codebase
343 # have regularly made that assumption as well. Note that we can't
344 # actually get rid of the nativeSys config option without breaking
345 # lots of on-disk camera configs.
346 assert detectorNativeSys == cameraGeom.PIXELS, "Detectors with nativeSys != PIXELS are not supported."
347 detectorNativeSys = cameraGeom.CameraSys(detectorNativeSys, detectorConfig.name)
349 return cameraBuilder.finish()