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