Hide keyboard shortcuts

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/>. 

21 

22__all__ = ["DetectorWrapper", "CameraWrapper"] 

23 

24import os 

25 

26import numpy as np 

27 

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 

41 

42 

43class DetectorWrapper: 

44 """A Detector and the data used to construct it 

45 

46 Intended for use with unit tests, thus saves a copy of all input parameters. 

47 Does not support setting details of amplifiers. 

48 

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 """ 

86 

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) 

118 

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] 

169 

170 

171class CameraWrapper: 

172 """A simple Camera and the data used to construct it 

173 

174 Intended for use with unit tests, thus saves some interesting information. 

175 

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 """ 

189 

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") 

194 

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 

201 

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) 

207 

208 @property 

209 def nDetectors(self): 

210 """Return the number of detectors""" 

211 return len(self.detectorNameList) 

212 

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 

254 

255 def makeAmpLists(self, ampFile, isLsstLike=False): 

256 """Construct a dict of list of Amplifer, one list per detector. 

257 

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") 

349 

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 

385 

386 def makeTestRepositoryItems(self, isLsstLike=False): 

387 """Make camera config and amp catalog dictionary, using default 

388 detector and amp files. 

389 

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 

418 

419 

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) 

436 

437 

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) 

450 

451 

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()) 

474 

475 

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) 

498 

499 

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) 

507 

508 

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())