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

309 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-16 03:19 -0700

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

40 

41 

42class DetectorWrapper: 

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

44 

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

46 Does not support setting details of amplifiers. 

47 

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

85 

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) 

117 

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] 

168 

169 

170class CameraWrapper: 

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

172 

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

174 

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

188 

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

193 

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 

200 

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) 

206 

207 @property 

208 def nDetectors(self): 

209 """Return the number of detectors""" 

210 return len(self.detectorNameList) 

211 

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.offset_z = float(detector['z']) 

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 line in fh: 

276 els = line.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(list(Point2D)) functions by evaluating them over a 

423 range of values. 

424 

425 Notes 

426 ----- 

427 Assumes the functions can be called with ``list[Point2D]`` and return 

428 ``list[Point2D]``. 

429 """ 

430 if maxVal is None: 

431 maxVal = -minVal 

432 dVal = (maxVal - minVal) / (nVal - 1) 

433 points = [] 

434 for xInd in range(nVal): 

435 x = minVal + (xInd * dVal) 

436 for yInd in range(nVal): 

437 y = minVal + (yInd * dVal) 

438 fromPoint = lsst.geom.Point2D(x, y) 

439 points.append(fromPoint) 

440 

441 vres1 = func1(points) 

442 vres2 = func2(points) 

443 for res1, res2 in zip(vres1, vres2): 

444 self.assertPairsAlmostEqual(res1, res2) 

445 

446 

447@inTestCase 

448def assertTransformMapsEqual(self, map1, map2, **kwds): 

449 """Compare two TransformMaps. 

450 """ 

451 self.assertEqual(list(map1), list(map2)) # compares the sets of CameraSys 

452 for sysFrom in map1: 

453 for sysTo in map1: 

454 with self.subTest(sysFrom=sysFrom, sysTo=sysTo): 

455 transform1 = map1.getTransform(sysFrom, sysTo) 

456 transform2 = map2.getTransform(sysFrom, sysTo) 

457 self.compare2DFunctions(transform1.applyForward, transform2.applyForward, **kwds) 

458 self.compare2DFunctions(transform1.applyInverse, transform2.applyInverse, **kwds) 

459 

460 

461@inTestCase 

462def assertAmplifiersEqual(self, amp1, amp2): 

463 self.assertEqual(amp1.getName(), amp2.getName()) 

464 self.assertEqual(amp1.getBBox(), amp2.getBBox()) 

465 self.assertFloatsEqual(amp1.getGain(), amp2.getGain(), ignoreNaNs=True) 

466 self.assertFloatsEqual(amp1.getReadNoise(), amp2.getReadNoise(), ignoreNaNs=True) 

467 self.assertFloatsEqual(amp1.getSaturation(), amp2.getSaturation(), ignoreNaNs=True) 

468 self.assertEqual(amp1.getReadoutCorner(), amp2.getReadoutCorner()) 

469 self.assertFloatsEqual(amp1.getSuspectLevel(), amp2.getSuspectLevel(), ignoreNaNs=True) 

470 self.assertEqual(amp1.getLinearityCoeffs().shape, amp2.getLinearityCoeffs().shape) 

471 self.assertFloatsEqual(amp1.getLinearityCoeffs(), amp2.getLinearityCoeffs(), ignoreNaNs=True) 

472 self.assertEqual(amp1.getLinearityType(), amp2.getLinearityType()) 

473 self.assertFloatsEqual(amp1.getLinearityThreshold(), amp2.getLinearityThreshold(), ignoreNaNs=True) 

474 self.assertFloatsEqual(amp1.getLinearityMaximum(), amp2.getLinearityMaximum(), ignoreNaNs=True) 

475 self.assertEqual(amp1.getLinearityUnits(), amp2.getLinearityUnits()) 

476 self.assertEqual(amp1.getRawBBox(), amp2.getRawBBox()) 

477 self.assertEqual(amp1.getRawDataBBox(), amp2.getRawDataBBox()) 

478 self.assertEqual(amp1.getRawFlipX(), amp2.getRawFlipX()) 

479 self.assertEqual(amp1.getRawFlipY(), amp2.getRawFlipY()) 

480 self.assertEqual(amp1.getRawHorizontalOverscanBBox(), amp2.getRawHorizontalOverscanBBox()) 

481 self.assertEqual(amp1.getRawVerticalOverscanBBox(), amp2.getRawVerticalOverscanBBox()) 

482 self.assertEqual(amp1.getRawPrescanBBox(), amp2.getRawPrescanBBox()) 

483 

484 

485@inTestCase 

486def assertDetectorsEqual(self, detector1, detector2, *, compareTransforms=True, **kwds): 

487 """Compare two Detectors. 

488 """ 

489 self.assertEqual(detector1.getName(), detector2.getName()) 

490 self.assertEqual(detector1.getId(), detector2.getId()) 

491 self.assertEqual(detector1.getSerial(), detector2.getSerial()) 

492 self.assertEqual(detector1.getPhysicalType(), detector2.getPhysicalType()) 

493 self.assertEqual(detector1.getBBox(), detector2.getBBox()) 

494 self.assertEqual(detector1.getPixelSize(), detector2.getPixelSize()) 

495 orientationIn = detector1.getOrientation() 

496 orientationOut = detector2.getOrientation() 

497 self.assertEqual(orientationIn.getFpPosition(), orientationOut.getFpPosition()) 

498 self.assertEqual(orientationIn.getReferencePoint(), orientationOut.getReferencePoint()) 

499 self.assertEqual(orientationIn.getYaw(), orientationOut.getYaw()) 

500 self.assertEqual(orientationIn.getPitch(), orientationOut.getPitch()) 

501 self.assertEqual(orientationIn.getRoll(), orientationOut.getRoll()) 

502 self.assertFloatsEqual(detector1.getCrosstalk(), detector2.getCrosstalk()) 

503 if compareTransforms: 

504 self.assertTransformMapsEqual(detector1.getTransformMap(), detector2.getTransformMap(), **kwds) 

505 self.assertEqual(len(detector1.getAmplifiers()), len(detector2.getAmplifiers())) 

506 for amp1, amp2 in zip(detector1.getAmplifiers(), detector2.getAmplifiers()): 

507 self.assertAmplifiersEqual(amp1, amp2) 

508 

509 

510@inTestCase 

511def assertDetectorCollectionsEqual(self, collection1, collection2, **kwds): 

512 """Compare two DetectorCollections. 

513 """ 

514 self.assertCountEqual(list(collection1.getNameIter()), list(collection2.getNameIter())) 

515 for k in collection1.getNameIter(): 

516 self.assertDetectorsEqual(collection1[k], collection2[k], **kwds) 

517 

518 

519@inTestCase 

520def assertCamerasEqual(self, camera1, camera2, **kwds): 

521 """Compare two Camers. 

522 """ 

523 self.assertDetectorCollectionsEqual(camera1, camera2, **kwds) 

524 self.assertTransformMapsEqual(camera1.getTransformMap(), camera2.getTransformMap()) 

525 self.assertEqual(camera1.getName(), camera2.getName()) 

526 self.assertEqual(camera1.getPupilFactoryName(), camera2.getPupilFactoryName())