Coverage for python/lsst/obs/base/yamlCamera.py: 9%
139 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-16 09:07 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-16 09:07 +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/>.
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 if len(ccd["offset"]) == 2:
100 detectorConfig.offset_x, detectorConfig.offset_y = ccd["offset"]
101 detectorConfig.offset_z = 0.0
102 else:
103 detectorConfig.offset_x, detectorConfig.offset_y, detectorConfig.offset_z = ccd["offset"]
104 detectorConfig.transposeDetector = ccd["transposeDetector"]
105 detectorConfig.pitchDeg = ccd["pitch"]
106 detectorConfig.yawDeg = ccd["yaw"]
107 detectorConfig.rollDeg = ccd["roll"]
108 if "crosstalk" in ccd:
109 detectorConfig.crosstalk = ccd["crosstalk"]
111 return detectorConfigs
114def makeAmplifierList(ccd):
115 """Construct a list of AmplifierBuilder objects"""
116 # Much of this will need to be filled in when we know it.
117 assert len(ccd) > 0
118 amp = list(ccd["amplifiers"].values())[0]
120 rawBBox = makeBBoxFromList(amp["rawBBox"]) # total in file
121 xRawExtent, yRawExtent = rawBBox.getDimensions()
123 readCorners = {
124 "LL": ReadoutCorner.LL,
125 "LR": ReadoutCorner.LR,
126 "UL": ReadoutCorner.UL,
127 "UR": ReadoutCorner.UR,
128 }
130 amplifierList = []
131 for name, amp in sorted(ccd["amplifiers"].items(), key=lambda x: x[1]["hdu"]):
132 amplifier = Amplifier.Builder()
133 amplifier.setName(name)
135 ix, iy = amp["ixy"]
136 perAmpData = amp["perAmpData"]
137 if perAmpData:
138 x0, y0 = 0, 0 # origin of data within each amp image
139 else:
140 x0, y0 = ix * xRawExtent, iy * yRawExtent
142 rawDataBBox = makeBBoxFromList(amp["rawDataBBox"]) # Photosensitive area
143 xDataExtent, yDataExtent = rawDataBBox.getDimensions()
144 amplifier.setBBox(
145 geom.BoxI(geom.PointI(ix * xDataExtent, iy * yDataExtent), rawDataBBox.getDimensions())
146 )
148 rawBBox = makeBBoxFromList(amp["rawBBox"])
149 rawBBox.shift(geom.ExtentI(x0, y0))
150 amplifier.setRawBBox(rawBBox)
152 rawDataBBox = makeBBoxFromList(amp["rawDataBBox"])
153 rawDataBBox.shift(geom.ExtentI(x0, y0))
154 amplifier.setRawDataBBox(rawDataBBox)
156 rawSerialOverscanBBox = makeBBoxFromList(amp["rawSerialOverscanBBox"])
157 rawSerialOverscanBBox.shift(geom.ExtentI(x0, y0))
158 amplifier.setRawHorizontalOverscanBBox(rawSerialOverscanBBox)
160 rawParallelOverscanBBox = makeBBoxFromList(amp["rawParallelOverscanBBox"])
161 rawParallelOverscanBBox.shift(geom.ExtentI(x0, y0))
162 amplifier.setRawVerticalOverscanBBox(rawParallelOverscanBBox)
164 rawSerialPrescanBBox = makeBBoxFromList(amp["rawSerialPrescanBBox"])
165 rawSerialPrescanBBox.shift(geom.ExtentI(x0, y0))
166 amplifier.setRawPrescanBBox(rawSerialPrescanBBox)
168 if perAmpData:
169 amplifier.setRawXYOffset(geom.Extent2I(ix * xRawExtent, iy * yRawExtent))
170 else:
171 amplifier.setRawXYOffset(geom.Extent2I(0, 0))
173 amplifier.setReadoutCorner(readCorners[amp["readCorner"]])
174 amplifier.setGain(amp["gain"])
175 amplifier.setReadNoise(amp["readNoise"])
176 amplifier.setSaturation(amp["saturation"])
177 amplifier.setSuspectLevel(amp.get("suspect", np.nan))
179 # flip data when assembling if needs be (e.g. data from the serial at
180 # the top of a CCD)
181 flipX, flipY = amp.get("flipXY")
183 amplifier.setRawFlipX(flipX)
184 amplifier.setRawFlipY(flipY)
185 # linearity placeholder stuff
186 amplifier.setLinearityCoeffs([float(val) for val in amp["linearityCoeffs"]])
187 amplifier.setLinearityType(amp["linearityType"])
188 amplifier.setLinearityThreshold(float(amp["linearityThreshold"]))
189 amplifier.setLinearityMaximum(float(amp["linearityMax"]))
190 amplifier.setLinearityUnits("DN")
191 amplifierList.append(amplifier)
192 return amplifierList
195def makeAmpInfoCatalog(ccd):
196 """Backward compatible name."""
197 return makeAmplifierList(ccd)
200def makeBBoxFromList(ylist):
201 """Given a list [(x0, y0), (xsize, ysize)], probably from a yaml file,
202 return a BoxI
203 """
204 (x0, y0), (xsize, ysize) = ylist
205 return geom.BoxI(geom.PointI(x0, y0), geom.ExtentI(xsize, ysize))
208def makeTransformDict(nativeSys, transformDict, plateScale):
209 """Make a dictionary of TransformPoint2ToPoint2s from yaml, mapping from
210 nativeSys
212 Parameters
213 ----------
214 nativeSys : `lsst.afw.cameraGeom.CameraSys`
215 transformDict : `dict`
216 A dict specifying parameters of transforms; keys are camera system
217 names.
218 plateScale : `lsst.geom.Angle`
219 The size of a pixel in angular units/mm (e.g. 20 arcsec/mm for LSST)
221 Returns
222 -------
223 transforms : `dict`
224 A dict of `lsst.afw.cameraGeom.CameraSys` :
225 `lsst.afw.geom.TransformPoint2ToPoint2`
227 The resulting dict's keys are `~lsst.afw.cameraGeom.CameraSys`,
228 and the values are Transforms *from* NativeSys *to* CameraSys
229 """
230 # As other comments note this is required, and this is one function where
231 # it's assumed
232 assert nativeSys == cameraGeom.FOCAL_PLANE, "Cameras with nativeSys != FOCAL_PLANE are not supported."
234 resMap = dict()
236 for key, transform in transformDict.items():
237 transformType = transform["transformType"]
238 knownTransformTypes = ["affine", "radial"]
239 if transformType not in knownTransformTypes:
240 raise RuntimeError(
241 "Saw unknown transform type for %s: %s (known types are: [%s])"
242 % (key, transform["transformType"], ", ".join(knownTransformTypes))
243 )
245 if transformType == "affine":
246 affine = geom.AffineTransform(np.array(transform["linear"]), np.array(transform["translation"]))
248 transform = afwGeom.makeTransform(affine)
249 elif transformType == "radial":
250 # radial coefficients of the form [0, 1 (no units), C2 (rad),
251 # usually 0, C3 (rad^2), ...]
252 # Radial distortion is modeled as a radial polynomial that converts
253 # from focal plane radius (in mm) to field angle (in radians).
254 # The provided coefficients are divided by the plate
255 # scale (in radians/mm) meaning that C1 is always 1.
256 radialCoeffs = np.array(transform["coeffs"])
258 radialCoeffs *= plateScale.asRadians()
259 transform = afwGeom.makeRadialTransform(radialCoeffs)
260 else:
261 raise RuntimeError(
262 'Impossible condition "%s" is not in: [%s])'
263 % (transform["transformType"], ", ".join(knownTransformTypes))
264 )
266 resMap[cameraGeom.CameraSys(key)] = transform
268 return resMap
271def makeCameraFromCatalogs(
272 cameraName,
273 detectorConfigList,
274 nativeSys,
275 transformDict,
276 amplifierDict,
277 pupilFactoryClass=cameraGeom.pupil.PupilFactory,
278):
279 """Construct a Camera instance from a dictionary of
280 detector name : `lsst.afw.cameraGeom.amplifier`
282 Parameters
283 ----------
284 cameraName : `str`
285 The name of the camera
286 detectorConfigList : `list`
287 A list of `lsst.afw.cameraGeom.cameraConfig.DetectorConfig`
288 nativeSys : `lsst.afw.cameraGeom.CameraSys`
289 The native transformation type; must be
290 `lsst.afw.cameraGeom.FOCAL_PLANE`
291 transformDict : `dict`
292 A dict of lsst.afw.cameraGeom.CameraSys :
293 `lsst.afw.geom.TransformPoint2ToPoint2`
294 amplifierDict : `dict`
295 A dictionary of detector name :
296 `lsst.afw.cameraGeom.Amplifier.Builder`
297 pupilFactoryClass : `type`, optional
298 Class to attach to camera;
299 `lsst.default afw.cameraGeom.PupilFactory`
301 Returns
302 -------
303 camera : `lsst.afw.cameraGeom.Camera`
304 New Camera instance.
306 Notes
307 ------
308 Copied from `lsst.afw.cameraGeom.cameraFactory` with permission and
309 encouragement from Jim Bosch.
310 """
312 # nativeSys=FOCAL_PLANE seems to be assumed in various places in this file
313 # (e.g. the definition of TAN_PIXELS), despite CameraConfig providing the
314 # illusion that it's configurable.
315 # Note that we can't actually get rid of the nativeSys config option
316 # without breaking lots of on-disk camera configs.
317 assert nativeSys == cameraGeom.FOCAL_PLANE, "Cameras with nativeSys != FOCAL_PLANE are not supported."
319 focalPlaneToField = transformDict[cameraGeom.FIELD_ANGLE]
321 cameraBuilder = Camera.Builder(cameraName)
322 cameraBuilder.setPupilFactoryClass(pupilFactoryClass)
324 # Ensure all transforms in the camera transform dict are included.
325 for toSys, transform in transformDict.items():
326 cameraBuilder.setTransformFromFocalPlaneTo(toSys, transform)
328 for detectorConfig in detectorConfigList:
329 # This should build all detector pixel -> focalPlane transforms.
330 cameraGeom.addDetectorBuilderFromConfig(
331 cameraBuilder, detectorConfig, amplifierDict[detectorConfig.name], focalPlaneToField
332 )
334 # For reasons I don't understand, some obs_ packages (e.g. HSC) set
335 # nativeSys to None for their detectors (which doesn't seem to be
336 # permitted by the config class!), but they really mean PIXELS. For
337 # backwards compatibility we use that as the default...
338 detectorNativeSys = detectorConfig.transformDict.nativeSys
339 detectorNativeSys = (
340 cameraGeom.PIXELS if detectorNativeSys is None else cameraGeom.CameraSysPrefix(detectorNativeSys)
341 )
343 # ...well, actually, it seems that we've always assumed down in C++
344 # that the answer is always PIXELS without ever checking that it is.
345 # So let's assert that it is, since there are hints all over this file
346 # (e.g. the definition of TAN_PIXELS) that other parts of the codebase
347 # have regularly made that assumption as well. Note that we can't
348 # actually get rid of the nativeSys config option without breaking
349 # lots of on-disk camera configs.
350 assert detectorNativeSys == cameraGeom.PIXELS, "Detectors with nativeSys != PIXELS are not supported."
351 detectorNativeSys = cameraGeom.CameraSys(detectorNativeSys, detectorConfig.name)
353 return cameraBuilder.finish()