LSSTApplications  18.0.0+106,18.0.0+50,19.0.0,19.0.0+1,19.0.0+10,19.0.0+11,19.0.0+13,19.0.0+17,19.0.0+2,19.0.0-1-g20d9b18+6,19.0.0-1-g425ff20,19.0.0-1-g5549ca4,19.0.0-1-g580fafe+6,19.0.0-1-g6fe20d0+1,19.0.0-1-g7011481+9,19.0.0-1-g8c57eb9+6,19.0.0-1-gb5175dc+11,19.0.0-1-gdc0e4a7+9,19.0.0-1-ge272bc4+6,19.0.0-1-ge3aa853,19.0.0-10-g448f008b,19.0.0-12-g6990b2c,19.0.0-2-g0d9f9cd+11,19.0.0-2-g3d9e4fb2+11,19.0.0-2-g5037de4,19.0.0-2-gb96a1c4+3,19.0.0-2-gd955cfd+15,19.0.0-3-g2d13df8,19.0.0-3-g6f3c7dc,19.0.0-4-g725f80e+11,19.0.0-4-ga671dab3b+1,19.0.0-4-gad373c5+3,19.0.0-5-ga2acb9c+2,19.0.0-5-gfe96e6c+2,w.2020.01
LSSTDataManagementBasePackage
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 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  amplifier.setSuspectLevel(amp.get('suspect', np.nan))
170 
171  # flip data when assembling if needs be (e.g. data from the serial at the top of a CCD)
172  flipX, flipY = amp.get("flipXY")
173 
174  amplifier.setRawFlipX(flipX)
175  amplifier.setRawFlipY(flipY)
176  # linearity placeholder stuff
177  amplifier.setLinearityCoeffs([float(val) for val in amp['linearityCoeffs']])
178  amplifier.setLinearityType(amp['linearityType'])
179  amplifier.setLinearityThreshold(float(amp['linearityThreshold']))
180  amplifier.setLinearityMaximum(float(amp['linearityMax']))
181  amplifier.setLinearityUnits("DN")
182  amplifierList.append(amplifier)
183  return amplifierList
184 
185 
187  """Backward compatible name.
188  """
189  return makeAmplifierList(ccd)
190 
191 
192 def makeBBoxFromList(ylist):
193  """Given a list [(x0, y0), (xsize, ysize)], probably from a yaml file,
194  return a BoxI
195  """
196  (x0, y0), (xsize, ysize) = ylist
197  return geom.BoxI(geom.PointI(x0, y0), geom.ExtentI(xsize, ysize))
198 
199 
200 def makeTransformDict(nativeSys, transformDict, plateScale):
201  """Make a dictionary of TransformPoint2ToPoint2s from yaml, mapping from nativeSys
202 
203  Parameters
204  ----------
205  nativeSys : `lsst.afw.cameraGeom.CameraSys`
206  transformDict : `dict`
207  A dict specifying parameters of transforms; keys are camera system names.
208  plateScale : `lsst.geom.Angle`
209  The size of a pixel in angular units/mm (e.g. 20 arcsec/mm for LSST)
210 
211  Returns
212  -------
213  transforms : `dict`
214  A dict of `lsst.afw.cameraGeom.CameraSys` : `lsst.afw.geom.TransformPoint2ToPoint2`
215 
216  The resulting dict's keys are `~lsst.afw.cameraGeom.CameraSys`,
217  and the values are Transforms *from* NativeSys *to* CameraSys
218  """
219  # As other comments note this is required, and this is one function where it's assumed
220  assert nativeSys == cameraGeom.FOCAL_PLANE, "Cameras with nativeSys != FOCAL_PLANE are not supported."
221 
222  resMap = dict()
223 
224  for key, transform in transformDict.items():
225  transformType = transform["transformType"]
226  knownTransformTypes = ["affine", "radial"]
227  if transformType not in knownTransformTypes:
228  raise RuntimeError("Saw unknown transform type for %s: %s (known types are: [%s])" % (
229  key, transform["transformType"], ", ".join(knownTransformTypes)))
230 
231  if transformType == "affine":
232  affine = geom.AffineTransform(np.array(transform["linear"]),
233  np.array(transform["translation"]))
234 
235  transform = afwGeom.makeTransform(affine)
236  elif transformType == "radial":
237  # radial coefficients of the form [0, 1 (no units), C2 (rad), usually 0, C3 (rad^2), ...]
238  # Radial distortion is modeled as a radial polynomial that converts from focal plane radius
239  # (in mm) to field angle (in radians). The provided coefficients are divided by the plate
240  # scale (in radians/mm) meaning that C1 is always 1.
241  radialCoeffs = np.array(transform["coeffs"])
242 
243  radialCoeffs *= plateScale.asRadians()
244  transform = afwGeom.makeRadialTransform(radialCoeffs)
245  else:
246  raise RuntimeError("Impossible condition \"%s\" is not in: [%s])" % (
247  transform["transformType"], ", ".join(knownTransformTypes)))
248 
249  resMap[cameraGeom.CameraSys(key)] = transform
250 
251  return resMap
252 
253 
254 def makeCameraFromCatalogs(cameraName, detectorConfigList, nativeSys, transformDict, amplifierDict,
255  pupilFactoryClass=cameraGeom.pupil.PupilFactory):
256  """Construct a Camera instance from a dictionary of
257  detector name : `lsst.afw.cameraGeom.amplifier`
258 
259  Parameters
260  ----------
261  cameraName : `str`
262  The name of the camera
263  detectorConfigList : `list`
264  A list of `lsst.afw.cameraGeom.cameraConfig.DetectorConfig`
265  nativeSys : `lsst.afw.cameraGeom.CameraSys`
266  The native transformation type; must be `lsst.afw.cameraGeom.FOCAL_PLANE`
267  transformDict : `dict`
268  A dict of lsst.afw.cameraGeom.CameraSys : `lsst.afw.geom.TransformPoint2ToPoint2`
269  amplifierDict : `dict`
270  A dictionary of detector name :
271  `lsst.afw.cameraGeom.Amplifier.Builder`
272  pupilFactoryClass : `type`, optional
273  Class to attach to camera;
274  `lsst.default afw.cameraGeom.PupilFactory`
275 
276  Returns
277  -------
278  camera : `lsst.afw.cameraGeom.Camera`
279  New Camera instance.
280 
281  Notes
282  ------
283  Copied from `lsst.afw.cameraGeom.cameraFactory` with permission and encouragement
284  from Jim Bosch
285  """
286 
287  # nativeSys=FOCAL_PLANE seems to be assumed in various places in this file
288  # (e.g. the definition of TAN_PIXELS), despite CameraConfig providing the
289  # illusion that it's configurable.
290  # Note that we can't actually get rid of the nativeSys config option
291  # without breaking lots of on-disk camera configs.
292  assert nativeSys == cameraGeom.FOCAL_PLANE, "Cameras with nativeSys != FOCAL_PLANE are not supported."
293 
294  focalPlaneToField = transformDict[cameraGeom.FIELD_ANGLE]
295 
296  cameraBuilder = Camera.Builder(cameraName)
297  cameraBuilder.setPupilFactoryClass(pupilFactoryClass)
298 
299  # Ensure all transforms in the camera transform dict are included.
300  for toSys, transform in transformDict.items():
301  cameraBuilder.setTransformFromFocalPlaneTo(toSys, transform)
302 
303  for detectorConfig in detectorConfigList:
304  # This should build all detector pixel -> focalPlane transforms.
305  cameraGeom.addDetectorBuilderFromConfig(cameraBuilder, detectorConfig,
306  amplifierDict[detectorConfig.name],
307  focalPlaneToField)
308 
309  # For reasons I don't understand, some obs_ packages (e.g. HSC) set
310  # nativeSys to None for their detectors (which doesn't seem to be
311  # permitted by the config class!), but they really mean PIXELS. For
312  # backwards compatibility we use that as the default...
313  detectorNativeSys = detectorConfig.transformDict.nativeSys
314  detectorNativeSys = (cameraGeom.PIXELS if detectorNativeSys is None else
315  cameraGeom.CameraSysPrefix(detectorNativeSys))
316 
317  # ...well, actually, it seems that we've always assumed down in C++
318  # that the answer is always PIXELS without ever checking that it is.
319  # So let's assert that it is, since there are hints all over this file
320  # (e.g. the definition of TAN_PIXELS) that other parts of the codebase
321  # have regularly made that assumption as well. Note that we can't
322  # actually get rid of the nativeSys config option without breaking
323  # lots of on-disk camera configs.
324  assert detectorNativeSys == cameraGeom.PIXELS, \
325  "Detectors with nativeSys != PIXELS are not supported."
326  detectorNativeSys = cameraGeom.CameraSys(detectorNativeSys, detectorConfig.name)
327 
328  return cameraBuilder.finish()
def makeBBoxFromList(ylist)
Definition: yamlCamera.py:192
An affine coordinate transformation consisting of a linear transformation and an offset.
std::vector< SchemaItem< Flag > > * items
def makeCamera(cameraFile)
Definition: yamlCamera.py:34
A class representing an angle.
Definition: Angle.h:127
A mutable Amplifier subclass class that can be used to incrementally construct or modify Amplifiers...
Definition: Amplifier.h:280
def makeDetectorConfigList(ccdParams)
Definition: yamlCamera.py:70
def makeTransformDict(nativeSys, transformDict, plateScale)
Definition: yamlCamera.py:200
std::shared_ptr< TransformPoint2ToPoint2 > makeRadialTransform(std::vector< double > const &forwardCoeffs, std::vector< double > const &inverseCoeffs)
A purely radial polynomial distortion.
def makeCameraFromCatalogs(cameraName, detectorConfigList, nativeSys, transformDict, amplifierDict, pupilFactoryClass=cameraGeom.pupil.PupilFactory)
Definition: yamlCamera.py:255
A helper class for creating and modifying cameras.
Definition: Camera.h:208
An integer coordinate rectangle.
Definition: Box.h:55
daf::base::PropertyList * list
Definition: fits.cc:903
std::shared_ptr< TransformPoint2ToPoint2 > makeTransform(lsst::geom::AffineTransform const &affine)
Wrap an lsst::geom::AffineTransform as a Transform.