LSSTApplications  16.0-10-g0ee56ad+5,16.0-11-ga33d1f2+5,16.0-12-g3ef5c14+3,16.0-12-g71e5ef5+18,16.0-12-gbdf3636+3,16.0-13-g118c103+3,16.0-13-g8f68b0a+3,16.0-15-gbf5c1cb+4,16.0-16-gfd17674+3,16.0-17-g7c01f5c+3,16.0-18-g0a50484+1,16.0-20-ga20f992+8,16.0-21-g0e05fd4+6,16.0-21-g15e2d33+4,16.0-22-g62d8060+4,16.0-22-g847a80f+4,16.0-25-gf00d9b8+1,16.0-28-g3990c221+4,16.0-3-gf928089+3,16.0-32-g88a4f23+5,16.0-34-gd7987ad+3,16.0-37-gc7333cb+2,16.0-4-g10fc685+2,16.0-4-g18f3627+26,16.0-4-g5f3a788+26,16.0-5-gaf5c3d7+4,16.0-5-gcc1f4bb+1,16.0-6-g3b92700+4,16.0-6-g4412fcd+3,16.0-6-g7235603+4,16.0-69-g2562ce1b+2,16.0-8-g14ebd58+4,16.0-8-g2df868b+1,16.0-8-g4cec79c+6,16.0-8-gadf6c7a+1,16.0-8-gfc7ad86,16.0-82-g59ec2a54a+1,16.0-9-g5400cdc+2,16.0-9-ge6233d7+5,master-g2880f2d8cf+3,v17.0.rc1
LSSTDataManagementBasePackage
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.table import AmpInfoCatalog, AmpInfoTable
29 from lsst.afw.cameraGeom.cameraFactory import makeDetectorData
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  ampInfoCatDict = {}
64  for ccdName, ccdValues in ccdParams.items():
65  ampInfoCatDict[ccdName] = makeAmpInfoCatalog(ccdValues)
66 
67  return makeCameraFromCatalogs(cameraName, detectorConfigList, nativeSys, transforms, ampInfoCatDict)
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  # This is the orientation we need to put the serial direction along the x-axis
88  detectorConfig.bbox_x0, detectorConfig.bbox_y0 = ccd['bbox'][0]
89  detectorConfig.bbox_x1, detectorConfig.bbox_y1 = ccd['bbox'][1]
90  detectorConfig.pixelSize_x, detectorConfig.pixelSize_y = ccd['pixelSize']
91  detectorConfig.transformDict.nativeSys = ccd['transformDict']['nativeSys']
92  transforms = ccd['transformDict']['transforms']
93  detectorConfig.transformDict.transforms = None if transforms == 'None' else transforms
94  detectorConfig.refpos_x, detectorConfig.refpos_y = ccd['refpos']
95  detectorConfig.offset_x, detectorConfig.offset_y = ccd['offset']
96  detectorConfig.transposeDetector = ccd['transposeDetector']
97  detectorConfig.pitchDeg = ccd['pitch']
98  detectorConfig.yawDeg = ccd['yaw']
99  detectorConfig.rollDeg = ccd['roll']
100  if 'crosstalk' in ccd:
101  detectorConfig.crosstalk = ccd['crosstalk']
102 
103  return detectorConfigs
104 
105 
107  """Construct an amplifier info catalog
108  """
109  # Much of this will need to be filled in when we know it.
110  assert len(ccd['amplifiers']) > 0
111  amp = list(ccd['amplifiers'].values())[0]
112 
113  rawBBox = makeBBoxFromList(amp['rawBBox']) # total in file
114  xRawExtent, yRawExtent = rawBBox.getDimensions()
115 
116  from lsst.afw.table import LL, LR, UL, UR
117  readCorners = dict(LL=LL, LR=LR, UL=UL, UR=UR)
118 
119  schema = AmpInfoTable.makeMinimalSchema()
120 
121  linThreshKey = schema.addField('linearityThreshold', type=float)
122  linMaxKey = schema.addField('linearityMaximum', type=float)
123  linUnitsKey = schema.addField('linearityUnits', type=str, size=9)
124  hduKey = schema.addField('hdu', type=np.int32)
125  # end placeholder
126 
127  ampCatalog = AmpInfoCatalog(schema)
128  for name, amp in sorted(ccd['amplifiers'].items(), key=lambda x: x[1]['hdu']):
129  record = ampCatalog.addNew()
130  record.setName(name)
131  record.set(hduKey, amp['hdu'])
132 
133  ix, iy = amp['ixy']
134  perAmpData = amp['perAmpData']
135  if perAmpData:
136  x0, y0 = 0, 0 # origin of data within each amp image
137  else:
138  x0, y0 = ix*xRawExtent, iy*yRawExtent
139 
140  rawDataBBox = makeBBoxFromList(amp['rawDataBBox']) # Photosensitive area
141  xDataExtent, yDataExtent = rawDataBBox.getDimensions()
142  record.setBBox(afwGeom.BoxI(
143  afwGeom.PointI(ix*xDataExtent, iy*yDataExtent), rawDataBBox.getDimensions()))
144 
145  rawBBox = makeBBoxFromList(amp['rawBBox'])
146  rawBBox.shift(afwGeom.ExtentI(x0, y0))
147  record.setRawBBox(rawBBox)
148 
149  rawDataBBox = makeBBoxFromList(amp['rawDataBBox'])
150  rawDataBBox.shift(afwGeom.ExtentI(x0, y0))
151  record.setRawDataBBox(rawDataBBox)
152 
153  rawSerialOverscanBBox = makeBBoxFromList(amp['rawSerialOverscanBBox'])
154  rawSerialOverscanBBox.shift(afwGeom.ExtentI(x0, y0))
155  record.setRawHorizontalOverscanBBox(rawSerialOverscanBBox)
156 
157  rawParallelOverscanBBox = makeBBoxFromList(amp['rawParallelOverscanBBox'])
158  rawParallelOverscanBBox.shift(afwGeom.ExtentI(x0, y0))
159  record.setRawVerticalOverscanBBox(rawParallelOverscanBBox)
160 
161  rawSerialPrescanBBox = makeBBoxFromList(amp['rawSerialPrescanBBox'])
162  rawSerialPrescanBBox.shift(afwGeom.ExtentI(x0, y0))
163  record.setRawPrescanBBox(rawSerialPrescanBBox)
164 
165  if perAmpData:
166  record.setRawXYOffset(afwGeom.Extent2I(ix*xRawExtent, iy*yRawExtent))
167  else:
168  record.setRawXYOffset(afwGeom.Extent2I(0, 0))
169 
170  record.setReadoutCorner(readCorners[amp['readCorner']])
171  record.setGain(amp['gain'])
172  record.setReadNoise(amp['readNoise'])
173  record.setSaturation(amp['saturation'])
174  record.setHasRawInfo(True)
175  # flip data when assembling if needs be (e.g. data from the serial at the top of a CCD)
176  flipX, flipY = amp.get("flipXY")
177 
178  record.setRawFlipX(flipX)
179  record.setRawFlipY(flipY)
180  # linearity placeholder stuff
181  record.setLinearityCoeffs([float(val) for val in amp['linearityCoeffs']])
182  record.setLinearityType(amp['linearityType'])
183  record.set(linThreshKey, float(amp['linearityThreshold']))
184  record.set(linMaxKey, float(amp['linearityMax']))
185  record.set(linUnitsKey, "DN")
186  return ampCatalog
187 
188 
189 def makeBBoxFromList(ylist):
190  """Given a list [(x0, y0), (xsize, ysize)], probably from a yaml file,
191  return a BoxI
192  """
193  (x0, y0), (xsize, ysize) = ylist
194  return afwGeom.BoxI(afwGeom.PointI(x0, y0), afwGeom.ExtentI(xsize, ysize))
195 
196 
197 def makeTransformDict(nativeSys, transformDict, plateScale):
198  """Make a dictionary of TransformPoint2ToPoint2s from yaml, mapping from nativeSys
199 
200  Parameters
201  ----------
202  nativeSys : `lsst.afw.cameraGeom.CameraSys`
203  transformDict : `dict`
204  A dict specifying parameters of transforms; keys are camera system names.
205  plateScale : `lsst.geom.Angle`
206  The size of a pixel in angular units/mm (e.g. 20 arcsec/mm for LSST)
207 
208  Returns
209  -------
210  transforms : `dict`
211  A dict of `lsst.afw.cameraGeom.CameraSys` : `lsst.afw.geom.TransformPoint2ToPoint2`
212 
213  The resulting dict's keys are `~lsst.afw.cameraGeom.CameraSys`,
214  and the values are Transforms *from* NativeSys *to* CameraSys
215  """
216  # As other comments note this is required, and this is one function where it's assumed
217  assert nativeSys == cameraGeom.FOCAL_PLANE, "Cameras with nativeSys != FOCAL_PLANE are not supported."
218 
219  resMap = dict()
220 
221  for key, transform in transformDict.items():
222  transformType = transform["transformType"]
223  knownTransformTypes = ["affine", "radial"]
224  if transformType not in knownTransformTypes:
225  raise RuntimeError("Saw unknown transform type for %s: %s (known types are: [%s])" % (
226  key, transform["transformType"], ", ".join(knownTransformTypes)))
227 
228  if transformType == "affine":
229  affine = geom.AffineTransform(np.array(transform["linear"]),
230  np.array(transform["translation"]))
231 
232  transform = afwGeom.makeTransform(affine)
233  elif transformType == "radial":
234  # radial coefficients of the form [0, 1 (no units), C2 (rad), usually 0, C3 (rad^2), ...]
235  # Radial distortion is modeled as a radial polynomial that converts from focal plane radius
236  # (in mm) to field angle (in radians). The provided coefficients are divided by the plate
237  # scale (in radians/mm) meaning that C1 is always 1.
238  radialCoeffs = np.array(transform["coeffs"])
239 
240  radialCoeffs *= plateScale.asRadians()
241  transform = afwGeom.makeRadialTransform(radialCoeffs)
242  else:
243  raise RuntimeError("Impossible condition \"%s\" is not in: [%s])" % (
244  transform["transformType"], ", ".join(knownTransformTypes)))
245 
246  resMap[cameraGeom.CameraSys(key)] = transform
247 
248  return resMap
249 
250 
251 def makeCameraFromCatalogs(cameraName, detectorConfigList, nativeSys, transformDict, ampInfoCatDict,
252  pupilFactoryClass=cameraGeom.pupil.PupilFactory):
253  """Construct a Camera instance from a dictionary of
254  detector name : `lsst.afw.table.ampInfo.AmpInfoCatalog`
255 
256  Parameters
257  ----------
258  cameraName : `str`
259  The name of the camera
260  detectorConfig : `list`
261  A list of `lsst.afw.cameraGeom.cameraConfig.DetectorConfig`
262  nativeSys : `lsst.afw.cameraGeom.CameraSys`
263  The native transformation type; must be `lsst.afw.cameraGeom.FOCAL_PLANE`
264  transformDict : `dict`
265  A dict of lsst.afw.cameraGeom.CameraSys : `lsst.afw.geom.TransformPoint2ToPoint2`
266  ampInfoCatDict : `dict`
267  A dictionary of detector name :
268  `lsst.afw.table.ampInfo.AmpInfoCatalog`
269  pupilFactoryClass : `type`, optional
270  Class to attach to camera;
271  `lsst.default afw.cameraGeom.PupilFactory`
272 
273  Returns
274  -------
275  camera : `lsst.afw.cameraGeom.Camera`
276  New Camera instance.
277 
278  Notes
279  ------
280  Copied from `lsst.afw.cameraGeom.cameraFactory` with permission and encouragement
281  from Jim Bosch
282  """
283 
284  # nativeSys=FOCAL_PLANE seems to be assumed in various places in this file
285  # (e.g. the definition of TAN_PIXELS), despite CameraConfig providing the
286  # illusion that it's configurable.
287  # Note that we can't actually get rid of the nativeSys config option
288  # without breaking lots of on-disk camera configs.
289  assert nativeSys == cameraGeom.FOCAL_PLANE, "Cameras with nativeSys != FOCAL_PLANE are not supported."
290 
291  focalPlaneToField = transformDict[cameraGeom.FIELD_ANGLE]
292  transformMapBuilder = cameraGeom.TransformMap.Builder(nativeSys)
293  transformMapBuilder.connect(transformDict)
294 
295  # First pass: build a list of all Detector ctor kwargs, minus the
296  # transformMap (which needs information from all Detectors).
297  detectorData = []
298  for detectorConfig in detectorConfigList:
299 
300  # Get kwargs that could be used to construct each Detector
301  # if we didn't care about giving each of them access to
302  # all of the transforms.
303  thisDetectorData = makeDetectorData(
304  detectorConfig=detectorConfig,
305  ampInfoCatalog=ampInfoCatDict[detectorConfig.name],
306  focalPlaneToField=focalPlaneToField,
307  )
308 
309  # Pull the transforms dictionary out of the data dict; we'll replace
310  # it with a TransformMap argument later.
311  thisDetectorTransforms = thisDetectorData.pop("transforms")
312 
313  # Save the rest of the Detector data dictionary for later
314  detectorData.append(thisDetectorData)
315 
316  # For reasons I don't understand, some obs_ packages (e.g. HSC) set
317  # nativeSys to None for their detectors (which doesn't seem to be
318  # permitted by the config class!), but they really mean PIXELS. For
319  # backwards compatibility we use that as the default...
320  detectorNativeSys = detectorConfig.transformDict.nativeSys
321  detectorNativeSys = (cameraGeom.PIXELS if detectorNativeSys is None else
322  cameraGeom.CameraSysPrefix(detectorNativeSys))
323 
324  # ...well, actually, it seems that we've always assumed down in C++
325  # that the answer is always PIXELS without ever checking that it is.
326  # So let's assert that it is, since there are hints all over this file
327  # (e.g. the definition of TAN_PIXELS) that other parts of the codebase
328  # have regularly made that assumption as well. Note that we can't
329  # actually get rid of the nativeSys config option without breaking
330  # lots of on-disk camera configs.
331  assert detectorNativeSys == cameraGeom.PIXELS, \
332  "Detectors with nativeSys != PIXELS are not supported."
333  detectorNativeSys = cameraGeom.CameraSys(detectorNativeSys, detectorConfig.name)
334 
335  # Add this detector's transform dict to the shared TransformMapBuilder
336  transformMapBuilder.connect(detectorNativeSys, thisDetectorTransforms)
337 
338  # Now that we've collected all of the Transforms, we can finally build the
339  # (immutable) TransformMap.
340  transformMap = transformMapBuilder.build()
341 
342  # Second pass through the detectorConfigs: actually make Detector instances
343  detectorList = [cameraGeom.Detector(transformMap=transformMap, **kw) for kw in detectorData]
344 
345  return cameraGeom.Camera(cameraName, detectorList, transformMap, pupilFactoryClass)
def makeBBoxFromList(ylist)
Definition: yamlCamera.py:189
def makeDetectorData(detectorConfig, ampInfoCatalog, focalPlaneToField)
An affine coordinate transformation consisting of a linear transformation and an offset.
def makeCamera(cameraFile)
Definition: yamlCamera.py:34
A class representing an angle.
Definition: Angle.h:127
def makeDetectorConfigList(ccdParams)
Definition: yamlCamera.py:70
def makeTransformDict(nativeSys, transformDict, plateScale)
Definition: yamlCamera.py:197
std::shared_ptr< TransformPoint2ToPoint2 > makeRadialTransform(std::vector< double > const &forwardCoeffs, std::vector< double > const &inverseCoeffs)
A purely radial polynomial distortion.
CatalogT< AmpInfoRecord > AmpInfoCatalog
Definition: fwd.h:97
def makeCameraFromCatalogs(cameraName, detectorConfigList, nativeSys, transformDict, ampInfoCatDict, pupilFactoryClass=cameraGeom.pupil.PupilFactory)
Definition: yamlCamera.py:252
std::vector< SchemaItem< Flag > > * items
An integer coordinate rectangle.
Definition: Box.h:54
daf::base::PropertyList * list
Definition: fits.cc:833
std::shared_ptr< TransformPoint2ToPoint2 > makeTransform(lsst::geom::AffineTransform const &affine)
Wrap an lsst::geom::AffineTransform as a Transform.