Coverage for python/lsst/afw/cameraGeom/testUtils.py : 10%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of afw.
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/>.
22__all__ = ["DetectorWrapper", "CameraWrapper"]
24import os
26import numpy as np
28import lsst.utils
29import lsst.geom
30import lsst.afw.geom as afwGeom
31from lsst.utils.tests import inTestCase
32from .cameraSys import CameraSys, PIXELS, TAN_PIXELS, FIELD_ANGLE, FOCAL_PLANE, ACTUAL_PIXELS
33from .orientation import Orientation
34from .amplifier import Amplifier, ReadoutCorner
35from .camera import Camera
36from .detector import DetectorType
37from .cameraConfig import DetectorConfig, CameraConfig
38from .cameraFactory import makeCameraFromAmpLists
39from .makePixelToTanPixel import makePixelToTanPixel
40from .transformConfig import TransformMapConfig
43class DetectorWrapper:
44 """A Detector and the data used to construct it
46 Intended for use with unit tests, thus saves a copy of all input parameters.
47 Does not support setting details of amplifiers.
49 Parameters
50 ----------
51 name : `str` (optional)
52 Detector name.
53 id : `int` (optional)
54 Detector ID.
55 detType : `lsst.afw.cameraGeom.DetectorType` (optional)
56 Detector type.
57 serial : `str` (optional)
58 Serial "number".
59 bbox : `lsst.geom.Box2I` (optional)
60 Bounding box; defaults to (0, 0), (1024x1024).
61 numAmps : `int` (optional)
62 Number of amplifiers.
63 pixelSize : `lsst.geom.Point2D` (optional)
64 Pixel size (mm).
65 ampExtent : `lsst.geom.Extent2I` (optional)
66 Dimensions of amplifier image bbox.
67 orientation : `lsst.afw.cameraGeom.Orientation` (optional)
68 Orientation of CCC in focal plane.
69 plateScale : `float` (optional)
70 Plate scale in arcsec/mm; 20.0 is for LSST.
71 radialDistortion : `float` (optional)
72 Radial distortion, in mm/rad^2.
73 The r^3 coefficient of the radial distortion polynomial
74 that converts FIELD_ANGLE in radians to FOCAL_PLANE in mm;
75 0.925 is the value Dave Monet measured for lsstSim data
76 crosstalk : `iterable` (optional)
77 Crosstalk coefficient matrix. If None, then no crosstalk correction
78 can be performed.
79 modFunc : `callable` (optional)
80 A function that can modify attributes just before constructing the
81 detector; modFunc receives one argument: a DetectorWrapper with all
82 attributes except detector set.
83 physicalType : `str` (optional)
84 The physical type of the device, e.g. CCD, E2V, HgCdTe
85 """
87 def __init__(self,
88 name="detector 1",
89 id=1,
90 detType=DetectorType.SCIENCE,
91 serial="xkcd722",
92 bbox=None, # do not use mutable objects as defaults
93 numAmps=3,
94 pixelSize=(0.02, 0.02),
95 ampExtent=(5, 6),
96 orientation=Orientation(),
97 plateScale=20.0,
98 radialDistortion=0.925,
99 crosstalk=None,
100 modFunc=None,
101 physicalType="CCD",
102 cameraBuilder=None
103 ):
104 # note that (0., 0.) for the reference position is the center of the
105 # first pixel
106 self.name = name
107 self.id = int(id)
108 self.type = detType
109 self.serial = serial
110 if bbox is None:
111 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), lsst.geom.Extent2I(1024, 1048))
112 self.bbox = bbox
113 self.pixelSize = lsst.geom.Extent2D(*pixelSize)
114 self.ampExtent = lsst.geom.Extent2I(*ampExtent)
115 self.plateScale = float(plateScale)
116 self.orientation = orientation
117 self.radialDistortion = float(radialDistortion)
119 # compute TAN_PIXELS transform
120 pScaleRad = lsst.geom.arcsecToRad(self.plateScale)
121 radialDistortCoeffs = [0.0, 1.0/pScaleRad,
122 0.0, self.radialDistortion/pScaleRad]
123 focalPlaneToField = afwGeom.makeRadialTransform(radialDistortCoeffs)
124 pixelToTanPixel = makePixelToTanPixel(
125 bbox=self.bbox,
126 orientation=self.orientation,
127 focalPlaneToField=focalPlaneToField,
128 pixelSizeMm=self.pixelSize,
129 )
130 tanPixelSys = CameraSys(TAN_PIXELS, self.name)
131 actualPixelSys = CameraSys(ACTUAL_PIXELS, self.name)
132 self.transMap = {
133 FOCAL_PLANE: self.orientation.makePixelFpTransform(self.pixelSize),
134 tanPixelSys: pixelToTanPixel,
135 actualPixelSys: afwGeom.makeRadialTransform([0, 0.95, 0.01]),
136 }
137 if crosstalk is None:
138 crosstalk = [[0.0 for _ in range(numAmps)] for _ in range(numAmps)]
139 self.crosstalk = crosstalk
140 self.physicalType = physicalType
141 if cameraBuilder is None:
142 cameraBuilder = Camera.Builder("CameraForDetectorWrapper")
143 self.ampList = []
144 for i in range(numAmps):
145 ampBuilder = Amplifier.Builder()
146 ampName = f"amp {i + 1}"
147 ampBuilder.setName(ampName)
148 ampBuilder.setBBox(lsst.geom.Box2I(lsst.geom.Point2I(-1, 1), self.ampExtent))
149 ampBuilder.setGain(1.71234e3)
150 ampBuilder.setReadNoise(0.521237e2)
151 ampBuilder.setReadoutCorner(ReadoutCorner.LL)
152 self.ampList.append(ampBuilder)
153 if modFunc:
154 modFunc(self)
155 detectorBuilder = cameraBuilder.add(self.name, self.id)
156 detectorBuilder.setType(self.type)
157 detectorBuilder.setSerial(self.serial)
158 detectorBuilder.setPhysicalType(self.physicalType)
159 detectorBuilder.setBBox(self.bbox)
160 detectorBuilder.setOrientation(self.orientation)
161 detectorBuilder.setPixelSize(self.pixelSize)
162 detectorBuilder.setTransformFromPixelsTo(tanPixelSys, self.transMap[tanPixelSys])
163 detectorBuilder.setTransformFromPixelsTo(actualPixelSys, self.transMap[actualPixelSys])
164 detectorBuilder.setCrosstalk(np.array(self.crosstalk, dtype=np.float32))
165 for ampBuilder in self.ampList:
166 detectorBuilder.append(ampBuilder)
167 camera = cameraBuilder.finish()
168 self.detector = camera[self.name]
171class CameraWrapper:
172 """A simple Camera and the data used to construct it
174 Intended for use with unit tests, thus saves some interesting information.
176 Parameters
177 ----------
178 plateScale : `float`
179 Plate scale in arcsec/mm; 20.0 is for LSST.
180 radialDistortion : `float`
181 Radial distortion, in mm/rad^2.
182 The r^3 coefficient of the radial distortion polynomial
183 that converts FIELD_ANGLE in radians to FOCAL_PLANE in mm;
184 0.925 is the value Dave Monet measured for lsstSim data.
185 isLsstLike : `bool`.
186 Make repository products with one raw image per amplifier (True)
187 or with one raw image per detector (False).
188 """
190 def __init__(self, plateScale=20.0, radialDistortion=0.925, isLsstLike=False):
191 afwDir = lsst.utils.getPackageDir("afw")
192 self._afwTestDataDir = os.path.join(afwDir, "python", "lsst", "afw",
193 "cameraGeom", "testData")
195 # Info to store for unit tests
196 self.plateScale = float(plateScale)
197 self.radialDistortion = float(radialDistortion)
198 self.detectorNameList = []
199 self.detectorIdList = []
200 self.ampDataDict = {} # ampData[Dict]: raw dictionaries of test data fields
202 # ampList[Dict]: actual cameraGeom.Amplifier objects
203 self.camConfig, self.ampListDict = self.makeTestRepositoryItems(
204 isLsstLike)
205 self.camera = makeCameraFromAmpLists(
206 self.camConfig, self.ampListDict)
208 @property
209 def nDetectors(self):
210 """Return the number of detectors"""
211 return len(self.detectorNameList)
213 def makeDetectorConfigs(self, detFile):
214 """Construct a list of DetectorConfig, one per detector
215 """
216 detectors = []
217 self.detectorNameList = []
218 self.detectorIdList = []
219 with open(detFile) as fh:
220 names = fh.readline().rstrip().lstrip("#").split("|")
221 for l in fh:
222 els = l.rstrip().split("|")
223 detectorProps = dict([(name, el)
224 for name, el in zip(names, els)])
225 detectors.append(detectorProps)
226 detectorConfigs = []
227 for i, detector in enumerate(detectors):
228 detectorId = (i + 1) * 10 # to avoid simple 0, 1, 2...
229 detectorName = detector['name']
230 detConfig = DetectorConfig()
231 detConfig.name = detectorName
232 detConfig.id = detectorId
233 detConfig.bbox_x0 = 0
234 detConfig.bbox_y0 = 0
235 detConfig.bbox_x1 = int(detector['npix_x']) - 1
236 detConfig.bbox_y1 = int(detector['npix_y']) - 1
237 detConfig.serial = str(detector['serial'])
238 detConfig.detectorType = int(detector['detectorType'])
239 detConfig.offset_x = float(detector['x'])
240 detConfig.offset_y = float(detector['y'])
241 detConfig.refpos_x = float(detector['refPixPos_x'])
242 detConfig.refpos_y = float(detector['refPixPos_y'])
243 detConfig.yawDeg = float(detector['yaw'])
244 detConfig.pitchDeg = float(detector['pitch'])
245 detConfig.rollDeg = float(detector['roll'])
246 detConfig.pixelSize_x = float(detector['pixelSize'])
247 detConfig.pixelSize_y = float(detector['pixelSize'])
248 detConfig.transposeDetector = False
249 detConfig.transformDict.nativeSys = PIXELS.getSysName()
250 detectorConfigs.append(detConfig)
251 self.detectorNameList.append(detectorName)
252 self.detectorIdList.append(detectorId)
253 return detectorConfigs
255 def makeAmpLists(self, ampFile, isLsstLike=False):
256 """Construct a dict of list of Amplifer, one list per detector.
258 Parameters
259 ----------
260 ampFile : `str`
261 Path to amplifier data file.
262 isLsstLike : `bool`
263 If True then there is one raw image per amplifier;
264 if False then there is one raw image per detector.
265 """
266 readoutMap = {
267 'LL': ReadoutCorner.LL,
268 'LR': ReadoutCorner.LR,
269 'UR': ReadoutCorner.UR,
270 'UL': ReadoutCorner.UL,
271 }
272 ampDataList = []
273 with open(ampFile) as fh:
274 names = fh.readline().rstrip().lstrip("#").split("|")
275 for l in fh:
276 els = l.rstrip().split("|")
277 ampProps = dict([(name, el) for name, el in zip(names, els)])
278 ampDataList.append(ampProps)
279 ampListDict = {}
280 self.ampDataDict = {}
281 for ampData in ampDataList:
282 if ampData['ccd_name'] in ampListDict:
283 ampList = ampListDict[ampData['ccd_name']]
284 self.ampDataDict[ampData['ccd_name']]['namps'] += 1
285 else:
286 ampList = []
287 ampListDict[ampData['ccd_name']] = ampList
288 self.ampDataDict[ampData['ccd_name']] = {'namps': 1, 'linInfo': {}}
289 builder = Amplifier.Builder()
290 bbox = lsst.geom.Box2I(lsst.geom.Point2I(int(ampData['trimmed_xmin']),
291 int(ampData['trimmed_ymin'])),
292 lsst.geom.Point2I(int(ampData['trimmed_xmax']),
293 int(ampData['trimmed_ymax'])))
294 rawBbox = lsst.geom.Box2I(lsst.geom.Point2I(int(ampData['raw_xmin']),
295 int(ampData['raw_ymin'])),
296 lsst.geom.Point2I(int(ampData['raw_xmax']),
297 int(ampData['raw_ymax'])))
298 rawDataBbox = lsst.geom.Box2I(
299 lsst.geom.Point2I(int(ampData['raw_data_xmin']),
300 int(ampData['raw_data_ymin'])),
301 lsst.geom.Point2I(int(ampData['raw_data_xmax']),
302 int(ampData['raw_data_ymax'])))
303 rawHOverscanBbox = lsst.geom.Box2I(
304 lsst.geom.Point2I(int(ampData['hoscan_xmin']),
305 int(ampData['hoscan_ymin'])),
306 lsst.geom.Point2I(int(ampData['hoscan_xmax']),
307 int(ampData['hoscan_ymax'])))
308 rawVOverscanBbox = lsst.geom.Box2I(
309 lsst.geom.Point2I(int(ampData['voscan_xmin']),
310 int(ampData['voscan_ymin'])),
311 lsst.geom.Point2I(int(ampData['voscan_xmax']),
312 int(ampData['voscan_ymax'])))
313 rawPrescanBbox = lsst.geom.Box2I(
314 lsst.geom.Point2I(int(ampData['pscan_xmin']),
315 int(ampData['pscan_ymin'])),
316 lsst.geom.Point2I(int(ampData['pscan_xmax']),
317 int(ampData['pscan_ymax'])))
318 xoffset = int(ampData['x_offset'])
319 yoffset = int(ampData['y_offset'])
320 flipx = bool(int(ampData['flipx']))
321 flipy = bool(int(ampData['flipy']))
322 readcorner = 'LL'
323 if not isLsstLike:
324 offext = lsst.geom.Extent2I(xoffset, yoffset)
325 if flipx:
326 xExt = rawBbox.getDimensions().getX()
327 rawBbox.flipLR(xExt)
328 rawDataBbox.flipLR(xExt)
329 rawHOverscanBbox.flipLR(xExt)
330 rawVOverscanBbox.flipLR(xExt)
331 rawPrescanBbox.flipLR(xExt)
332 if flipy:
333 yExt = rawBbox.getDimensions().getY()
334 rawBbox.flipTB(yExt)
335 rawDataBbox.flipTB(yExt)
336 rawHOverscanBbox.flipTB(yExt)
337 rawVOverscanBbox.flipTB(yExt)
338 rawPrescanBbox.flipTB(yExt)
339 if not flipx and not flipy:
340 readcorner = 'LL'
341 elif flipx and not flipy:
342 readcorner = 'LR'
343 elif flipx and flipy:
344 readcorner = 'UR'
345 elif not flipx and flipy:
346 readcorner = 'UL'
347 else:
348 raise RuntimeError("Couldn't find read corner")
350 flipx = False
351 flipy = False
352 rawBbox.shift(offext)
353 rawDataBbox.shift(offext)
354 rawHOverscanBbox.shift(offext)
355 rawVOverscanBbox.shift(offext)
356 rawPrescanBbox.shift(offext)
357 xoffset = 0
358 yoffset = 0
359 offset = lsst.geom.Extent2I(xoffset, yoffset)
360 builder.setBBox(bbox)
361 builder.setRawXYOffset(offset)
362 builder.setName(str(ampData['name']))
363 builder.setReadoutCorner(readoutMap[readcorner])
364 builder.setGain(float(ampData['gain']))
365 builder.setReadNoise(float(ampData['readnoise']))
366 linCoeffs = np.array([float(ampData['lin_coeffs']), ], dtype=float)
367 builder.setLinearityCoeffs(linCoeffs)
368 builder.setLinearityType(str(ampData['lin_type']))
369 builder.setRawFlipX(flipx)
370 builder.setRawFlipY(flipy)
371 builder.setRawBBox(rawBbox)
372 builder.setRawDataBBox(rawDataBbox)
373 builder.setRawHorizontalOverscanBBox(rawHOverscanBbox)
374 builder.setRawVerticalOverscanBBox(rawVOverscanBbox)
375 builder.setRawPrescanBBox(rawPrescanBbox)
376 builder.setLinearityThreshold(float(ampData['lin_thresh']))
377 builder.setLinearityMaximum(float(ampData['lin_max']))
378 builder.setLinearityUnits(str(ampData['lin_units']))
379 self.ampDataDict[ampData['ccd_name']]['linInfo'][ampData['name']] = \
380 {'lincoeffs': linCoeffs, 'lintype': str(ampData['lin_type']),
381 'linthresh': float(ampData['lin_thresh']), 'linmax': float(ampData['lin_max']),
382 'linunits': str(ampData['lin_units'])}
383 ampList.append(builder)
384 return ampListDict
386 def makeTestRepositoryItems(self, isLsstLike=False):
387 """Make camera config and amp catalog dictionary, using default
388 detector and amp files.
390 Parameters
391 ----------
392 isLsstLike : `bool`
393 If True then there is one raw image per amplifier;
394 if False then there is one raw image per detector.
395 """
396 detFile = os.path.join(self._afwTestDataDir, "testCameraDetectors.dat")
397 detectorConfigs = self.makeDetectorConfigs(detFile)
398 ampFile = os.path.join(self._afwTestDataDir, "testCameraAmps.dat")
399 ampListDict = self.makeAmpLists(ampFile, isLsstLike=isLsstLike)
400 camConfig = CameraConfig()
401 camConfig.name = "testCamera%s"%('LSST' if isLsstLike else 'SC')
402 camConfig.detectorList = dict((i, detConfig)
403 for i, detConfig in enumerate(detectorConfigs))
404 camConfig.plateScale = self.plateScale
405 pScaleRad = lsst.geom.arcsecToRad(self.plateScale)
406 radialDistortCoeffs = [0.0, 1.0/pScaleRad,
407 0.0, self.radialDistortion/pScaleRad]
408 tConfig = afwGeom.TransformConfig()
409 tConfig.transform.name = 'inverted'
410 radialClass = afwGeom.transformRegistry['radial']
411 tConfig.transform.active.transform.retarget(radialClass)
412 tConfig.transform.active.transform.coeffs = radialDistortCoeffs
413 tmc = TransformMapConfig()
414 tmc.nativeSys = FOCAL_PLANE.getSysName()
415 tmc.transforms = {FIELD_ANGLE.getSysName(): tConfig}
416 camConfig.transformDict = tmc
417 return camConfig, ampListDict
420@inTestCase
421def compare2DFunctions(self, func1, func2, minVal=-10, maxVal=None, nVal=5):
422 """Compare two Point2D(Point2D) functions by evaluating them over a
423 range of values.
424 """
425 if maxVal is None:
426 maxVal = -minVal
427 dVal = (maxVal - minVal) / (nVal - 1)
428 for xInd in range(nVal):
429 x = minVal + (xInd * dVal)
430 for yInd in range(nVal):
431 y = minVal + (yInd * dVal)
432 fromPoint = lsst.geom.Point2D(x, y)
433 res1 = func1(fromPoint)
434 res2 = func2(fromPoint)
435 self.assertPairsAlmostEqual(res1, res2)
438@inTestCase
439def assertTransformMapsEqual(self, map1, map2, **kwds):
440 """Compare two TransformMaps.
441 """
442 self.assertEqual(list(map1), list(map2)) # compares the sets of CameraSys
443 for sysFrom in map1:
444 for sysTo in map1:
445 with self.subTest(sysFrom=sysFrom, sysTo=sysTo):
446 transform1 = map1.getTransform(sysFrom, sysTo)
447 transform2 = map2.getTransform(sysFrom, sysTo)
448 self.compare2DFunctions(transform1.applyForward, transform2.applyForward, **kwds)
449 self.compare2DFunctions(transform1.applyInverse, transform2.applyInverse, **kwds)
452@inTestCase
453def assertAmplifiersEqual(self, amp1, amp2):
454 self.assertEqual(amp1.getName(), amp2.getName())
455 self.assertEqual(amp1.getBBox(), amp2.getBBox())
456 self.assertEqual(amp1.getGain(), amp2.getGain())
457 self.assertEqual(amp1.getReadNoise(), amp2.getReadNoise())
458 self.assertEqual(amp1.getSaturation(), amp2.getSaturation())
459 self.assertEqual(amp1.getReadoutCorner(), amp2.getReadoutCorner())
460 self.assertEqual(amp1.getSuspectLevel(), amp2.getSuspectLevel())
461 self.assertEqual(amp1.getLinearityCoeffs().shape, amp2.getLinearityCoeffs().shape)
462 self.assertFloatsEqual(amp1.getLinearityCoeffs(), amp2.getLinearityCoeffs())
463 self.assertEqual(amp1.getLinearityType(), amp2.getLinearityType())
464 self.assertEqual(amp1.getLinearityThreshold(), amp2.getLinearityThreshold())
465 self.assertEqual(amp1.getLinearityMaximum(), amp2.getLinearityMaximum())
466 self.assertEqual(amp1.getLinearityUnits(), amp2.getLinearityUnits())
467 self.assertEqual(amp1.getRawBBox(), amp2.getRawBBox())
468 self.assertEqual(amp1.getRawDataBBox(), amp2.getRawDataBBox())
469 self.assertEqual(amp1.getRawFlipX(), amp2.getRawFlipX())
470 self.assertEqual(amp1.getRawFlipY(), amp2.getRawFlipY())
471 self.assertEqual(amp1.getRawHorizontalOverscanBBox(), amp2.getRawHorizontalOverscanBBox())
472 self.assertEqual(amp1.getRawVerticalOverscanBBox(), amp2.getRawVerticalOverscanBBox())
473 self.assertEqual(amp1.getRawPrescanBBox(), amp2.getRawPrescanBBox())
476@inTestCase
477def assertDetectorsEqual(self, detector1, detector2, **kwds):
478 """Compare two Detectors.
479 """
480 self.assertEqual(detector1.getName(), detector2.getName())
481 self.assertEqual(detector1.getId(), detector2.getId())
482 self.assertEqual(detector1.getSerial(), detector2.getSerial())
483 self.assertEqual(detector1.getPhysicalType(), detector2.getPhysicalType())
484 self.assertEqual(detector1.getBBox(), detector2.getBBox())
485 self.assertEqual(detector1.getPixelSize(), detector2.getPixelSize())
486 orientationIn = detector1.getOrientation()
487 orientationOut = detector2.getOrientation()
488 self.assertEqual(orientationIn.getFpPosition(), orientationOut.getFpPosition())
489 self.assertEqual(orientationIn.getReferencePoint(), orientationOut.getReferencePoint())
490 self.assertEqual(orientationIn.getYaw(), orientationOut.getYaw())
491 self.assertEqual(orientationIn.getPitch(), orientationOut.getPitch())
492 self.assertEqual(orientationIn.getRoll(), orientationOut.getRoll())
493 self.assertFloatsEqual(detector1.getCrosstalk(), detector2.getCrosstalk())
494 self.assertTransformMapsEqual(detector1.getTransformMap(), detector2.getTransformMap(), **kwds)
495 self.assertEqual(len(detector1.getAmplifiers()), len(detector2.getAmplifiers()))
496 for amp1, amp2 in zip(detector1.getAmplifiers(), detector2.getAmplifiers()):
497 self.assertAmplifiersEqual(amp1, amp2)
500@inTestCase
501def assertDetectorCollectionsEqual(self, collection1, collection2, **kwds):
502 """Compare two DetectorCollections.
503 """
504 self.assertCountEqual(list(collection1.getNameIter()), list(collection2.getNameIter()))
505 for k in collection1.getNameIter():
506 self.assertDetectorsEqual(collection1[k], collection2[k], **kwds)
509@inTestCase
510def assertCamerasEqual(self, camera1, camera2, **kwds):
511 """Compare two Camers.
512 """
513 self.assertDetectorCollectionsEqual(camera1, camera2, **kwds)
514 self.assertTransformMapsEqual(camera1.getTransformMap(), camera2.getTransformMap())
515 self.assertEqual(camera1.getName(), camera2.getName())
516 self.assertEqual(camera1.getPupilFactoryName(), camera2.getPupilFactoryName())