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