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

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/>.
22import yaml
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
32__all__ = ["makeCamera"]
35@lru_cache()
36def makeCamera(cameraFile):
37 """An imaging camera (e.g. the LSST 3Gpix camera)
39 Parameters
40 ----------
41 cameraFile : `str`
42 Camera description YAML file.
44 Returns
45 -------
46 camera : `lsst.afw.cameraGeom.Camera`
47 The desired Camera
48 """
50 with open(cameraFile) as fd:
51 cameraParams = yaml.load(fd, Loader=yaml.CLoader)
53 cameraName = cameraParams["name"]
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)
62 ccdParams = cameraParams["CCDs"]
63 detectorConfigList = makeDetectorConfigList(ccdParams)
65 amplifierDict = {}
66 for ccdName, ccdValues in ccdParams.items():
67 amplifierDict[ccdName] = makeAmplifierList(ccdValues)
69 return makeCameraFromCatalogs(cameraName, detectorConfigList, nativeSys, transforms, amplifierDict)
72def makeDetectorConfigList(ccdParams):
73 """Make a list of detector configs
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)
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']
108 return detectorConfigs
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]
118 rawBBox = makeBBoxFromList(amp['rawBBox']) # total in file
119 xRawExtent, yRawExtent = rawBBox.getDimensions()
121 readCorners = {"LL": ReadoutCorner.LL,
122 "LR": ReadoutCorner.LR,
123 "UL": ReadoutCorner.UL,
124 "UR": ReadoutCorner.UR}
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(geom.BoxI(
141 geom.PointI(ix*xDataExtent, iy*yDataExtent), rawDataBBox.getDimensions()))
143 rawBBox = makeBBoxFromList(amp['rawBBox'])
144 rawBBox.shift(geom.ExtentI(x0, y0))
145 amplifier.setRawBBox(rawBBox)
147 rawDataBBox = makeBBoxFromList(amp['rawDataBBox'])
148 rawDataBBox.shift(geom.ExtentI(x0, y0))
149 amplifier.setRawDataBBox(rawDataBBox)
151 rawSerialOverscanBBox = makeBBoxFromList(amp['rawSerialOverscanBBox'])
152 rawSerialOverscanBBox.shift(geom.ExtentI(x0, y0))
153 amplifier.setRawHorizontalOverscanBBox(rawSerialOverscanBBox)
155 rawParallelOverscanBBox = makeBBoxFromList(amp['rawParallelOverscanBBox'])
156 rawParallelOverscanBBox.shift(geom.ExtentI(x0, y0))
157 amplifier.setRawVerticalOverscanBBox(rawParallelOverscanBBox)
159 rawSerialPrescanBBox = makeBBoxFromList(amp['rawSerialPrescanBBox'])
160 rawSerialPrescanBBox.shift(geom.ExtentI(x0, y0))
161 amplifier.setRawPrescanBBox(rawSerialPrescanBBox)
163 if perAmpData:
164 amplifier.setRawXYOffset(geom.Extent2I(ix*xRawExtent, iy*yRawExtent))
165 else:
166 amplifier.setRawXYOffset(geom.Extent2I(0, 0))
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))
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")
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
190def makeAmpInfoCatalog(ccd):
191 """Backward compatible name.
192 """
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("Saw unknown transform type for %s: %s (known types are: [%s])" % (
237 key, transform["transformType"], ", ".join(knownTransformTypes)))
239 if transformType == "affine":
240 affine = geom.AffineTransform(np.array(transform["linear"]),
241 np.array(transform["translation"]))
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"])
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)))
259 resMap[cameraGeom.CameraSys(key)] = transform
261 return resMap
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`
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`
288 Returns
289 -------
290 camera : `lsst.afw.cameraGeom.Camera`
291 New Camera instance.
293 Notes
294 ------
295 Copied from `lsst.afw.cameraGeom.cameraFactory` with permission and
296 encouragement from Jim Bosch.
297 """
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."
306 focalPlaneToField = transformDict[cameraGeom.FIELD_ANGLE]
308 cameraBuilder = Camera.Builder(cameraName)
309 cameraBuilder.setPupilFactoryClass(pupilFactoryClass)
311 # Ensure all transforms in the camera transform dict are included.
312 for toSys, transform in transformDict.items():
313 cameraBuilder.setTransformFromFocalPlaneTo(toSys, transform)
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)
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))
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)
340 return cameraBuilder.finish()