lsst.obs.base  18.1.0-24-ged780bc+4
yamlCamera.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2017 LSST Corporation.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <http://www.lsstcorp.org/LegalNotices/>.
21 #
22 import yaml
23 
24 import numpy as np
25 import lsst.afw.cameraGeom as cameraGeom
26 import lsst.geom as geom
27 import lsst.afw.geom as afwGeom
28 from lsst.afw.cameraGeom import Amplifier, Camera, ReadoutCorner
29 
30 
31 __all__ = ["makeCamera"]
32 
33 
34 def makeCamera(cameraFile):
35  """An imaging camera (e.g. the LSST 3Gpix camera)
36 
37  Parameters
38  ----------
39  cameraFile : `str`
40  Camera description YAML file.
41 
42  Returns
43  -------
44  camera : `lsst.afw.cameraGeom.Camera`
45  The desired Camera
46  """
47 
48  with open(cameraFile) as fd:
49  cameraParams = yaml.load(fd, Loader=yaml.CLoader)
50 
51  cameraName = cameraParams["name"]
52 
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)
59 
60  ccdParams = cameraParams["CCDs"]
61  detectorConfigList = makeDetectorConfigList(ccdParams)
62 
63  amplifierDict = {}
64  for ccdName, ccdValues in ccdParams.items():
65  amplifierDict[ccdName] = makeAmplifierList(ccdValues)
66 
67  return makeCameraFromCatalogs(cameraName, detectorConfigList, nativeSys, transforms, amplifierDict)
68 
69 
70 def makeDetectorConfigList(ccdParams):
71  """Make a list of detector configs
72 
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)
82 
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 the x-axis
90  detectorConfig.bbox_x0, detectorConfig.bbox_y0 = ccd['bbox'][0]
91  detectorConfig.bbox_x1, detectorConfig.bbox_y1 = ccd['bbox'][1]
92  detectorConfig.pixelSize_x, detectorConfig.pixelSize_y = ccd['pixelSize']
93  detectorConfig.transformDict.nativeSys = ccd['transformDict']['nativeSys']
94  transforms = ccd['transformDict']['transforms']
95  detectorConfig.transformDict.transforms = None if transforms == 'None' else transforms
96  detectorConfig.refpos_x, detectorConfig.refpos_y = ccd['refpos']
97  detectorConfig.offset_x, detectorConfig.offset_y = ccd['offset']
98  detectorConfig.transposeDetector = ccd['transposeDetector']
99  detectorConfig.pitchDeg = ccd['pitch']
100  detectorConfig.yawDeg = ccd['yaw']
101  detectorConfig.rollDeg = ccd['roll']
102  if 'crosstalk' in ccd:
103  detectorConfig.crosstalk = ccd['crosstalk']
104 
105  return detectorConfigs
106 
107 
109  """Construct a list of AmplifierBuilder objects
110  """
111  # Much of this will need to be filled in when we know it.
112  assert len(ccd) > 0
113  amp = list(ccd['amplifiers'].values())[0]
114 
115  rawBBox = makeBBoxFromList(amp['rawBBox']) # total in file
116  xRawExtent, yRawExtent = rawBBox.getDimensions()
117 
118  readCorners = {"LL": ReadoutCorner.LL,
119  "LR": ReadoutCorner.LR,
120  "UL": ReadoutCorner.UL,
121  "UR": ReadoutCorner.UR}
122 
123  amplifierList = []
124  for name, amp in sorted(ccd['amplifiers'].items(), key=lambda x: x[1]['hdu']):
125  amplifier = Amplifier.Builder()
126  amplifier.setName(name)
127 
128  ix, iy = amp['ixy']
129  perAmpData = amp['perAmpData']
130  if perAmpData:
131  x0, y0 = 0, 0 # origin of data within each amp image
132  else:
133  x0, y0 = ix*xRawExtent, iy*yRawExtent
134 
135  rawDataBBox = makeBBoxFromList(amp['rawDataBBox']) # Photosensitive area
136  xDataExtent, yDataExtent = rawDataBBox.getDimensions()
137  amplifier.setBBox(geom.BoxI(
138  geom.PointI(ix*xDataExtent, iy*yDataExtent), rawDataBBox.getDimensions()))
139 
140  rawBBox = makeBBoxFromList(amp['rawBBox'])
141  rawBBox.shift(geom.ExtentI(x0, y0))
142  amplifier.setRawBBox(rawBBox)
143 
144  rawDataBBox = makeBBoxFromList(amp['rawDataBBox'])
145  rawDataBBox.shift(geom.ExtentI(x0, y0))
146  amplifier.setRawDataBBox(rawDataBBox)
147 
148  rawSerialOverscanBBox = makeBBoxFromList(amp['rawSerialOverscanBBox'])
149  rawSerialOverscanBBox.shift(geom.ExtentI(x0, y0))
150  amplifier.setRawHorizontalOverscanBBox(rawSerialOverscanBBox)
151 
152  rawParallelOverscanBBox = makeBBoxFromList(amp['rawParallelOverscanBBox'])
153  rawParallelOverscanBBox.shift(geom.ExtentI(x0, y0))
154  amplifier.setRawVerticalOverscanBBox(rawParallelOverscanBBox)
155 
156  rawSerialPrescanBBox = makeBBoxFromList(amp['rawSerialPrescanBBox'])
157  rawSerialPrescanBBox.shift(geom.ExtentI(x0, y0))
158  amplifier.setRawPrescanBBox(rawSerialPrescanBBox)
159 
160  if perAmpData:
161  amplifier.setRawXYOffset(geom.Extent2I(ix*xRawExtent, iy*yRawExtent))
162  else:
163  amplifier.setRawXYOffset(geom.Extent2I(0, 0))
164 
165  amplifier.setReadoutCorner(readCorners[amp['readCorner']])
166  amplifier.setGain(amp['gain'])
167  amplifier.setReadNoise(amp['readNoise'])
168  amplifier.setSaturation(amp['saturation'])
169 
170  # flip data when assembling if needs be (e.g. data from the serial at the top of a CCD)
171  flipX, flipY = amp.get("flipXY")
172 
173  amplifier.setRawFlipX(flipX)
174  amplifier.setRawFlipY(flipY)
175  # linearity placeholder stuff
176  amplifier.setLinearityCoeffs([float(val) for val in amp['linearityCoeffs']])
177  amplifier.setLinearityType(amp['linearityType'])
178  amplifier.setLinearityThreshold(float(amp['linearityThreshold']))
179  amplifier.setLinearityMaximum(float(amp['linearityMax']))
180  amplifier.setLinearityUnits("DN")
181  amplifierList.append(amplifier)
182  return amplifierList
183 
184 
186  """Backward compatible name.
187  """
188  return makeAmplifierList(ccd)
189 
190 
191 def makeBBoxFromList(ylist):
192  """Given a list [(x0, y0), (xsize, ysize)], probably from a yaml file,
193  return a BoxI
194  """
195  (x0, y0), (xsize, ysize) = ylist
196  return geom.BoxI(geom.PointI(x0, y0), geom.ExtentI(xsize, ysize))
197 
198 
199 def makeTransformDict(nativeSys, transformDict, plateScale):
200  """Make a dictionary of TransformPoint2ToPoint2s from yaml, mapping from nativeSys
201 
202  Parameters
203  ----------
204  nativeSys : `lsst.afw.cameraGeom.CameraSys`
205  transformDict : `dict`
206  A dict specifying parameters of transforms; keys are camera system names.
207  plateScale : `lsst.geom.Angle`
208  The size of a pixel in angular units/mm (e.g. 20 arcsec/mm for LSST)
209 
210  Returns
211  -------
212  transforms : `dict`
213  A dict of `lsst.afw.cameraGeom.CameraSys` : `lsst.afw.geom.TransformPoint2ToPoint2`
214 
215  The resulting dict's keys are `~lsst.afw.cameraGeom.CameraSys`,
216  and the values are Transforms *from* NativeSys *to* CameraSys
217  """
218  # As other comments note this is required, and this is one function where it's assumed
219  assert nativeSys == cameraGeom.FOCAL_PLANE, "Cameras with nativeSys != FOCAL_PLANE are not supported."
220 
221  resMap = dict()
222 
223  for key, transform in transformDict.items():
224  transformType = transform["transformType"]
225  knownTransformTypes = ["affine", "radial"]
226  if transformType not in knownTransformTypes:
227  raise RuntimeError("Saw unknown transform type for %s: %s (known types are: [%s])" % (
228  key, transform["transformType"], ", ".join(knownTransformTypes)))
229 
230  if transformType == "affine":
231  affine = geom.AffineTransform(np.array(transform["linear"]),
232  np.array(transform["translation"]))
233 
234  transform = afwGeom.makeTransform(affine)
235  elif transformType == "radial":
236  # radial coefficients of the form [0, 1 (no units), C2 (rad), usually 0, C3 (rad^2), ...]
237  # Radial distortion is modeled as a radial polynomial that converts from focal plane radius
238  # (in mm) to field angle (in radians). The provided coefficients are divided by the plate
239  # scale (in radians/mm) meaning that C1 is always 1.
240  radialCoeffs = np.array(transform["coeffs"])
241 
242  radialCoeffs *= plateScale.asRadians()
243  transform = afwGeom.makeRadialTransform(radialCoeffs)
244  else:
245  raise RuntimeError("Impossible condition \"%s\" is not in: [%s])" % (
246  transform["transformType"], ", ".join(knownTransformTypes)))
247 
248  resMap[cameraGeom.CameraSys(key)] = transform
249 
250  return resMap
251 
252 
253 def makeCameraFromCatalogs(cameraName, detectorConfigList, nativeSys, transformDict, amplifierDict,
254  pupilFactoryClass=cameraGeom.pupil.PupilFactory):
255  """Construct a Camera instance from a dictionary of
256  detector name : `lsst.afw.cameraGeom.amplifier`
257 
258  Parameters
259  ----------
260  cameraName : `str`
261  The name of the camera
262  detectorConfigList : `list`
263  A list of `lsst.afw.cameraGeom.cameraConfig.DetectorConfig`
264  nativeSys : `lsst.afw.cameraGeom.CameraSys`
265  The native transformation type; must be `lsst.afw.cameraGeom.FOCAL_PLANE`
266  transformDict : `dict`
267  A dict of lsst.afw.cameraGeom.CameraSys : `lsst.afw.geom.TransformPoint2ToPoint2`
268  amplifierDict : `dict`
269  A dictionary of detector name :
270  `lsst.afw.cameraGeom.Amplifier.Builder`
271  pupilFactoryClass : `type`, optional
272  Class to attach to camera;
273  `lsst.default afw.cameraGeom.PupilFactory`
274 
275  Returns
276  -------
277  camera : `lsst.afw.cameraGeom.Camera`
278  New Camera instance.
279 
280  Notes
281  ------
282  Copied from `lsst.afw.cameraGeom.cameraFactory` with permission and encouragement
283  from Jim Bosch
284  """
285 
286  # nativeSys=FOCAL_PLANE seems to be assumed in various places in this file
287  # (e.g. the definition of TAN_PIXELS), despite CameraConfig providing the
288  # illusion that it's configurable.
289  # Note that we can't actually get rid of the nativeSys config option
290  # without breaking lots of on-disk camera configs.
291  assert nativeSys == cameraGeom.FOCAL_PLANE, "Cameras with nativeSys != FOCAL_PLANE are not supported."
292 
293  focalPlaneToField = transformDict[cameraGeom.FIELD_ANGLE]
294 
295  cameraBuilder = Camera.Builder(cameraName)
296  cameraBuilder.setPupilFactoryClass(pupilFactoryClass)
297 
298  # Ensure all transforms in the camera transform dict are included.
299  for toSys, transform in transformDict.items():
300  cameraBuilder.setTransformFromFocalPlaneTo(toSys, transform)
301 
302  for detectorConfig in detectorConfigList:
303  # This should build all detector pixel -> focalPlane transforms.
304  cameraGeom.addDetectorBuilderFromConfig(cameraBuilder, detectorConfig,
305  amplifierDict[detectorConfig.name],
306  focalPlaneToField)
307 
308  # For reasons I don't understand, some obs_ packages (e.g. HSC) set
309  # nativeSys to None for their detectors (which doesn't seem to be
310  # permitted by the config class!), but they really mean PIXELS. For
311  # backwards compatibility we use that as the default...
312  detectorNativeSys = detectorConfig.transformDict.nativeSys
313  detectorNativeSys = (cameraGeom.PIXELS if detectorNativeSys is None else
314  cameraGeom.CameraSysPrefix(detectorNativeSys))
315 
316  # ...well, actually, it seems that we've always assumed down in C++
317  # that the answer is always PIXELS without ever checking that it is.
318  # So let's assert that it is, since there are hints all over this file
319  # (e.g. the definition of TAN_PIXELS) that other parts of the codebase
320  # have regularly made that assumption as well. Note that we can't
321  # actually get rid of the nativeSys config option without breaking
322  # lots of on-disk camera configs.
323  assert detectorNativeSys == cameraGeom.PIXELS, \
324  "Detectors with nativeSys != PIXELS are not supported."
325  detectorNativeSys = cameraGeom.CameraSys(detectorNativeSys, detectorConfig.name)
326 
327  return cameraBuilder.finish()
def makeBBoxFromList(ylist)
Definition: yamlCamera.py:191
def makeCamera(cameraFile)
Definition: yamlCamera.py:34
def makeDetectorConfigList(ccdParams)
Definition: yamlCamera.py:70
def makeTransformDict(nativeSys, transformDict, plateScale)
Definition: yamlCamera.py:199
def makeCameraFromCatalogs(cameraName, detectorConfigList, nativeSys, transformDict, amplifierDict, pupilFactoryClass=cameraGeom.pupil.PupilFactory)
Definition: yamlCamera.py:254