Coverage for python/lsst/obs/base/yamlCamera.py: 10%
139 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-03 02:54 -0700
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-03 02:54 -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 """Construct 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 """
48 with open(cameraFile) as fd:
49 cameraParams = yaml.load(fd, Loader=yaml.CLoader)
51 cameraName = cameraParams["name"]
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)
60 ccdParams = cameraParams["CCDs"]
61 detectorConfigList = makeDetectorConfigList(ccdParams)
63 amplifierDict = {}
64 for ccdName, ccdValues in ccdParams.items():
65 amplifierDict[ccdName] = makeAmplifierList(ccdValues)
67 return makeCameraFromCatalogs(cameraName, detectorConfigList, nativeSys, transforms, amplifierDict)
70def makeDetectorConfigList(ccdParams):
71 """Make a list of detector configs.
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)
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"]
110 return detectorConfigs
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]
119 rawBBox = makeBBoxFromList(amp["rawBBox"]) # total in file
120 xRawExtent, yRawExtent = rawBBox.getDimensions()
122 readCorners = {
123 "LL": ReadoutCorner.LL,
124 "LR": ReadoutCorner.LR,
125 "UL": ReadoutCorner.UL,
126 "UR": ReadoutCorner.UR,
127 }
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)
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
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 )
147 rawBBox = makeBBoxFromList(amp["rawBBox"])
148 rawBBox.shift(geom.ExtentI(x0, y0))
149 amplifier.setRawBBox(rawBBox)
151 rawDataBBox = makeBBoxFromList(amp["rawDataBBox"])
152 rawDataBBox.shift(geom.ExtentI(x0, y0))
153 amplifier.setRawDataBBox(rawDataBBox)
155 rawSerialOverscanBBox = makeBBoxFromList(amp["rawSerialOverscanBBox"])
156 rawSerialOverscanBBox.shift(geom.ExtentI(x0, y0))
157 amplifier.setRawHorizontalOverscanBBox(rawSerialOverscanBBox)
159 rawParallelOverscanBBox = makeBBoxFromList(amp["rawParallelOverscanBBox"])
160 rawParallelOverscanBBox.shift(geom.ExtentI(x0, y0))
161 amplifier.setRawVerticalOverscanBBox(rawParallelOverscanBBox)
163 rawSerialPrescanBBox = makeBBoxFromList(amp["rawSerialPrescanBBox"])
164 rawSerialPrescanBBox.shift(geom.ExtentI(x0, y0))
165 amplifier.setRawPrescanBBox(rawSerialPrescanBBox)
167 if perAmpData:
168 amplifier.setRawXYOffset(geom.Extent2I(ix * xRawExtent, iy * yRawExtent))
169 else:
170 amplifier.setRawXYOffset(geom.Extent2I(0, 0))
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))
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")
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
194def makeAmpInfoCatalog(ccd):
195 """Backward compatible name."""
196 return makeAmplifierList(ccd)
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))
207def makeTransformDict(nativeSys, transformDict, plateScale):
208 """Make a dictionary of TransformPoint2ToPoint2s from yaml, mapping from
209 nativeSys.
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)
220 Returns
221 -------
222 transforms : `dict`
223 A dict of `lsst.afw.cameraGeom.CameraSys` :
224 `lsst.afw.geom.TransformPoint2ToPoint2`
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."
233 resMap = {}
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 {}: {} (known types are: [{}])".format(
241 key, transform["transformType"], ", ".join(knownTransformTypes)
242 )
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
251 # [0, 1 (no units), C2 (mm^-1), usually 0, C4 (mm^-3), ...]
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 "{}" is not in: [{}])'.format(
263 transform["transformType"], ", ".join(knownTransformTypes)
264 )
265 )
267 resMap[cameraGeom.CameraSys(key)] = transform
269 return resMap
272def makeCameraFromCatalogs(
273 cameraName,
274 detectorConfigList,
275 nativeSys,
276 transformDict,
277 amplifierDict,
278 pupilFactoryClass=cameraGeom.pupil.PupilFactory,
279):
280 """Construct a Camera instance from a dictionary of
281 detector name and `lsst.afw.cameraGeom.Amplifier`.
283 Parameters
284 ----------
285 cameraName : `str`
286 The name of the camera
287 detectorConfigList : `list`
288 A list of `lsst.afw.cameraGeom.cameraConfig.DetectorConfig`
289 nativeSys : `lsst.afw.cameraGeom.CameraSys`
290 The native transformation type; must be
291 `lsst.afw.cameraGeom.FOCAL_PLANE`
292 transformDict : `dict`
293 A dict of lsst.afw.cameraGeom.CameraSys :
294 `lsst.afw.geom.TransformPoint2ToPoint2`
295 amplifierDict : `dict` [`str`, `lsst.afw.cameraGeom.Amplifier.Builder` ]
296 A dictionary of detector name and amplifier builders.
297 pupilFactoryClass : `type` [ `lsst.default afw.cameraGeom.PupilFactory`], \
298 optional
299 Class to attach to camera.
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 """
311 # nativeSys=FOCAL_PLANE seems to be assumed in various places in this file
312 # (e.g. the definition of TAN_PIXELS), despite CameraConfig providing the
313 # illusion that it's configurable.
314 # Note that we can't actually get rid of the nativeSys config option
315 # without breaking lots of on-disk camera configs.
316 assert nativeSys == cameraGeom.FOCAL_PLANE, "Cameras with nativeSys != FOCAL_PLANE are not supported."
318 focalPlaneToField = transformDict[cameraGeom.FIELD_ANGLE]
320 cameraBuilder = Camera.Builder(cameraName)
321 cameraBuilder.setPupilFactoryClass(pupilFactoryClass)
323 # Ensure all transforms in the camera transform dict are included.
324 for toSys, transform in transformDict.items():
325 cameraBuilder.setTransformFromFocalPlaneTo(toSys, transform)
327 for detectorConfig in detectorConfigList:
328 # This should build all detector pixel -> focalPlane transforms.
329 cameraGeom.addDetectorBuilderFromConfig(
330 cameraBuilder, detectorConfig, amplifierDict[detectorConfig.name], focalPlaneToField
331 )
333 # For reasons I don't understand, some obs_ packages (e.g. HSC) set
334 # nativeSys to None for their detectors (which doesn't seem to be
335 # permitted by the config class!), but they really mean PIXELS. For
336 # backwards compatibility we use that as the default...
337 detectorNativeSys = detectorConfig.transformDict.nativeSys
338 detectorNativeSys = (
339 cameraGeom.PIXELS if detectorNativeSys is None else cameraGeom.CameraSysPrefix(detectorNativeSys)
340 )
342 # ...well, actually, it seems that we've always assumed down in C++
343 # that the answer is always PIXELS without ever checking that it is.
344 # So let's assert that it is, since there are hints all over this file
345 # (e.g. the definition of TAN_PIXELS) that other parts of the codebase
346 # have regularly made that assumption as well. Note that we can't
347 # actually get rid of the nativeSys config option without breaking
348 # lots of on-disk camera configs.
349 assert detectorNativeSys == cameraGeom.PIXELS, "Detectors with nativeSys != PIXELS are not supported."
350 detectorNativeSys = cameraGeom.CameraSys(detectorNativeSys, detectorConfig.name)
352 return cameraBuilder.finish()