LSST Applications  21.0.0+75b29a8a7f,21.0.0+e70536a077,21.0.0-1-ga51b5d4+62c747d40b,21.0.0-10-gbfb87ad6+3307648ee3,21.0.0-15-gedb9d5423+47cba9fc36,21.0.0-2-g103fe59+fdf0863a2a,21.0.0-2-g1367e85+d38a93257c,21.0.0-2-g45278ab+e70536a077,21.0.0-2-g5242d73+d38a93257c,21.0.0-2-g7f82c8f+e682ffb718,21.0.0-2-g8dde007+d179fbfa6a,21.0.0-2-g8f08a60+9402881886,21.0.0-2-ga326454+e682ffb718,21.0.0-2-ga63a54e+08647d4b1b,21.0.0-2-gde069b7+26c92b3210,21.0.0-2-gecfae73+0445ed2f95,21.0.0-2-gfc62afb+d38a93257c,21.0.0-27-gbbd0d29+ae871e0f33,21.0.0-28-g5fc5e037+feb0e9397b,21.0.0-3-g21c7a62+f4b9c0ff5c,21.0.0-3-g357aad2+57b0bddf0b,21.0.0-3-g4be5c26+d38a93257c,21.0.0-3-g65f322c+3f454acf5d,21.0.0-3-g7d9da8d+75b29a8a7f,21.0.0-3-gaa929c8+9e4ef6332c,21.0.0-3-ge02ed75+4b120a55c4,21.0.0-4-g3300ddd+e70536a077,21.0.0-4-g591bb35+4b120a55c4,21.0.0-4-gc004bbf+4911b9cd27,21.0.0-4-gccdca77+f94adcd104,21.0.0-4-ge8fba5a+2b3a696ff9,21.0.0-5-gb155db7+2c5429117a,21.0.0-5-gdf36809+637e4641ee,21.0.0-6-g00874e7+c9fd7f7160,21.0.0-6-g4e60332+4b120a55c4,21.0.0-7-gc8ca178+40eb9cf840,21.0.0-8-gfbe0b4b+9e4ef6332c,21.0.0-9-g2fd488a+d83b7cd606,w.2021.05
LSST Data Management Base Package
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
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']
107 
108  return detectorConfigs
109 
110 
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]
117 
118  rawBBox = makeBBoxFromList(amp['rawBBox']) # total in file
119  xRawExtent, yRawExtent = rawBBox.getDimensions()
120 
121  readCorners = {"LL": ReadoutCorner.LL,
122  "LR": ReadoutCorner.LR,
123  "UL": ReadoutCorner.UL,
124  "UR": ReadoutCorner.UR}
125 
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)
130 
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
137 
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()))
142 
143  rawBBox = makeBBoxFromList(amp['rawBBox'])
144  rawBBox.shift(geom.ExtentI(x0, y0))
145  amplifier.setRawBBox(rawBBox)
146 
147  rawDataBBox = makeBBoxFromList(amp['rawDataBBox'])
148  rawDataBBox.shift(geom.ExtentI(x0, y0))
149  amplifier.setRawDataBBox(rawDataBBox)
150 
151  rawSerialOverscanBBox = makeBBoxFromList(amp['rawSerialOverscanBBox'])
152  rawSerialOverscanBBox.shift(geom.ExtentI(x0, y0))
153  amplifier.setRawHorizontalOverscanBBox(rawSerialOverscanBBox)
154 
155  rawParallelOverscanBBox = makeBBoxFromList(amp['rawParallelOverscanBBox'])
156  rawParallelOverscanBBox.shift(geom.ExtentI(x0, y0))
157  amplifier.setRawVerticalOverscanBBox(rawParallelOverscanBBox)
158 
159  rawSerialPrescanBBox = makeBBoxFromList(amp['rawSerialPrescanBBox'])
160  rawSerialPrescanBBox.shift(geom.ExtentI(x0, y0))
161  amplifier.setRawPrescanBBox(rawSerialPrescanBBox)
162 
163  if perAmpData:
164  amplifier.setRawXYOffset(geom.Extent2I(ix*xRawExtent, iy*yRawExtent))
165  else:
166  amplifier.setRawXYOffset(geom.Extent2I(0, 0))
167 
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))
173 
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")
177 
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
188 
189 
191  """Backward compatible name.
192  """
193  return makeAmplifierList(ccd)
194 
195 
196 def 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))
202 
203 
204 def makeTransformDict(nativeSys, transformDict, plateScale):
205  """Make a dictionary of TransformPoint2ToPoint2s from yaml, mapping from
206  nativeSys
207 
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)
216 
217  Returns
218  -------
219  transforms : `dict`
220  A dict of `lsst.afw.cameraGeom.CameraSys` :
221  `lsst.afw.geom.TransformPoint2ToPoint2`
222 
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."
229 
230  resMap = dict()
231 
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)))
238 
239  if transformType == "affine":
240  affine = geom.AffineTransform(np.array(transform["linear"]),
241  np.array(transform["translation"]))
242 
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"])
252 
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)))
258 
259  resMap[cameraGeom.CameraSys(key)] = transform
260 
261  return resMap
262 
263 
264 def 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`
268 
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`
287 
288  Returns
289  -------
290  camera : `lsst.afw.cameraGeom.Camera`
291  New Camera instance.
292 
293  Notes
294  ------
295  Copied from `lsst.afw.cameraGeom.cameraFactory` with permission and
296  encouragement from Jim Bosch.
297  """
298 
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."
305 
306  focalPlaneToField = transformDict[cameraGeom.FIELD_ANGLE]
307 
308  cameraBuilder = Camera.Builder(cameraName)
309  cameraBuilder.setPupilFactoryClass(pupilFactoryClass)
310 
311  # Ensure all transforms in the camera transform dict are included.
312  for toSys, transform in transformDict.items():
313  cameraBuilder.setTransformFromFocalPlaneTo(toSys, transform)
314 
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)
320 
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))
328 
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)
339 
340  return cameraBuilder.finish()
std::vector< SchemaItem< Flag > > * items
A mutable Amplifier subclass class that can be used to incrementally construct or modify Amplifiers.
Definition: Amplifier.h:276
A helper class for creating and modifying cameras.
Definition: Camera.h:208
An affine coordinate transformation consisting of a linear transformation and an offset.
A class representing an angle.
Definition: Angle.h:127
An integer coordinate rectangle.
Definition: Box.h:55
std::shared_ptr< TransformPoint2ToPoint2 > makeTransform(lsst::geom::AffineTransform const &affine)
Wrap an lsst::geom::AffineTransform as a Transform.
std::shared_ptr< TransformPoint2ToPoint2 > makeRadialTransform(std::vector< double > const &coeffs)
A purely radial polynomial distortion.
def makeCameraFromCatalogs(cameraName, detectorConfigList, nativeSys, transformDict, amplifierDict, pupilFactoryClass=cameraGeom.pupil.PupilFactory)
Definition: yamlCamera.py:265
def makeDetectorConfigList(ccdParams)
Definition: yamlCamera.py:72
def makeCamera(cameraFile)
Definition: yamlCamera.py:36
def makeBBoxFromList(ylist)
Definition: yamlCamera.py:196
def makeTransformDict(nativeSys, transformDict, plateScale)
Definition: yamlCamera.py:204
daf::base::PropertyList * list
Definition: fits.cc:913