Coverage for python / lsst / images / tests / _checks.py: 10%

381 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-23 01:30 -0700

1# This file is part of lsst-images. 

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# Use of this source code is governed by a 3-clause BSD-style 

10# license that can be found in the LICENSE file. 

11 

12from __future__ import annotations 

13 

14__all__ = ( 

15 "arrays_to_legacy_points", 

16 "assert_close", 

17 "assert_equal_allow_nan", 

18 "assert_images_equal", 

19 "assert_masked_images_equal", 

20 "assert_masks_equal", 

21 "assert_projections_equal", 

22 "assert_psfs_equal", 

23 "check_astropy_wcs_interface", 

24 "check_projection", 

25 "check_transform", 

26 "compare_amplifier_to_legacy", 

27 "compare_aperture_corrections_to_legacy", 

28 "compare_cell_coadd_to_legacy", 

29 "compare_detector_to_legacy", 

30 "compare_field_to_legacy", 

31 "compare_image_to_legacy", 

32 "compare_mask_to_legacy", 

33 "compare_masked_image_to_legacy", 

34 "compare_observation_summary_stats_to_legacy", 

35 "compare_photo_calib_to_legacy", 

36 "compare_projection_to_legacy_wcs", 

37 "compare_psf_to_legacy", 

38 "compare_visit_image_to_legacy", 

39 "legacy_coords_to_astropy", 

40 "legacy_points_to_xy_array", 

41) 

42 

43import dataclasses 

44import math 

45import unittest 

46from collections.abc import Mapping 

47from typing import TYPE_CHECKING, Any, Literal, cast 

48 

49import astropy.units as u 

50import astropy.wcs.wcsapi 

51import numpy as np 

52from astropy.coordinates import SkyCoord 

53 

54from .._geom import XY, YX, BoundsError, Box 

55from .._image import Image 

56from .._mask import Mask, MaskPlane 

57from .._masked_image import MaskedImage 

58from .._observation_summary_stats import ObservationSummaryStats 

59from .._transforms import DetectorFrame, Frame, Projection, SkyFrame, TractFrame, Transform 

60from .._visit_image import VisitImage 

61from ..aperture_corrections import ApertureCorrectionMap 

62from ..cameras import Amplifier, Detector, DetectorType, ReadoutCorner 

63from ..cells import CellCoadd, CellIJ, CoaddProvenance 

64from ..fields import BaseField, ChebyshevField 

65from ..psfs import PointSpreadFunction 

66 

67if TYPE_CHECKING: 

68 try: 

69 from lsst.cell_coadds import MultipleCellCoadd 

70 except ImportError: 

71 type MultipleCellCoadd = Any # type: ignore[no-redef] 

72 try: 

73 from lsst.afw.image import PhotoCalib as LegacyPhotoCalib 

74 except ImportError: 

75 type LegacyPhotoCalib = Any # type: ignore[no-redef] 

76 

77 

78def assert_close( 

79 tc: unittest.TestCase, 

80 a: np.ndarray | u.Quantity | float, 

81 b: np.ndarray | u.Quantity | float, 

82 **kwargs: Any, 

83) -> None: 

84 """Test that two arrays, floats, or quantities are almost equal. 

85 

86 Parameters 

87 ---------- 

88 tc 

89 Test case object with assert methods to use. 

90 a 

91 Array, float, or quantity to compare. 

92 b 

93 Array, float, or quantity to compare. 

94 **kwargs 

95 Forwarded to `astropy.units.allclose`. 

96 """ 

97 tc.assertTrue(u.allclose(a, b, **kwargs), msg=f"{a} != {b}") 

98 

99 

100def assert_equal_allow_nan(tc: unittest.TestCase, a: float, b: float) -> None: 

101 """Test that two floating point values are equal, with nan == nan.""" 

102 try: 

103 tc.assertEqual(a, b) 

104 except AssertionError: 

105 if not (math.isnan(a) and math.isnan(b)): 

106 raise 

107 

108 

109def assert_images_equal( 

110 tc: unittest.TestCase, 

111 a: Image, 

112 b: Image, 

113 *, 

114 rtol: float = 0.0, 

115 atol: float = 0.0, 

116 expect_view: bool | Literal["array"] | None = None, 

117) -> None: 

118 """Assert that two images are equal or nearly equal.""" 

119 tc.assertEqual(a.bbox, b.bbox) 

120 tc.assertEqual(a.unit, b.unit) 

121 assert_projections_equal(tc, a.projection, b.projection) 

122 if expect_view is not None: 

123 tc.assertEqual(np.may_share_memory(a.array, b.array), bool(expect_view)) 

124 if expect_view == "array": 

125 tc.assertEqual(a.metadata, b.metadata) 

126 else: 

127 tc.assertEqual(a.metadata is b.metadata, expect_view) 

128 if not expect_view: 

129 assert_close(tc, a.array, b.array, atol=atol, rtol=rtol) 

130 tc.assertEqual(a.metadata, b.metadata) 

131 

132 

133def assert_masks_equal(tc: unittest.TestCase, a: Mask, b: Mask) -> None: 

134 """Assert that two masks are equal or nearly equal.""" 

135 tc.assertEqual(a.bbox, b.bbox) 

136 tc.assertEqual(a.schema, b.schema) 

137 tc.assertEqual(a.metadata, b.metadata) 

138 assert_projections_equal(tc, a.projection, b.projection) 

139 np.testing.assert_array_equal(a.array, b.array) 

140 

141 

142def assert_masked_images_equal( 

143 tc: unittest.TestCase, 

144 a: MaskedImage, 

145 b: MaskedImage, 

146 *, 

147 rtol: float = 0.0, 

148 atol: float = 0.0, 

149 expect_view: bool | None = None, 

150) -> None: 

151 """Assert that two masked images are equal or nearly equal.""" 

152 tc.assertEqual(a.metadata, b.metadata) 

153 assert_projections_equal(tc, a.projection, b.projection) 

154 assert_images_equal(tc, a.image, b.image, rtol=rtol, atol=atol, expect_view=expect_view) 

155 assert_masks_equal(tc, a.mask, b.mask) 

156 assert_images_equal(tc, a.variance, b.variance, rtol=rtol, atol=atol, expect_view=expect_view) 

157 

158 

159def assert_psfs_equal( 

160 tc: unittest.TestCase, 

161 psf1: PointSpreadFunction, 

162 psf2: PointSpreadFunction, 

163 points: YX[np.ndarray] | XY[np.ndarray] | None = None, 

164) -> int: 

165 """Compare two PSF objets. 

166 

167 Parameters 

168 ---------- 

169 tc 

170 Test case object with assert methods to use. 

171 psf1 

172 Point-spread function to test. 

173 psf2 

174 The other point-spread function to test. 

175 points 

176 Points to evaluate the PSFs at. If not provided, the intersection of 

177 the PSF bounding boxes are used to generate points on a grid. 

178 

179 Returns 

180 ------- 

181 `int` 

182 The number of points actually tested. 

183 """ 

184 if points is None: 

185 points = psf1.bounds.bbox.intersection(psf1.bounds.bbox).meshgrid(3).map(np.ravel) 

186 

187 tc.assertEqual(psf1.kernel_bbox, psf2.kernel_bbox) 

188 

189 n_points_tested: int = 0 

190 for x, y in zip(points.x, points.y): 

191 if not psf1.bounds.contains(x=x, y=y): 

192 with tc.assertRaises(BoundsError): 

193 psf2.compute_kernel_image(x=x, y=y) 

194 continue 

195 tc.assertEqual(psf1.compute_kernel_image(x=x, y=y), psf2.compute_kernel_image(x=x, y=y)) 

196 tc.assertEqual(psf1.compute_stellar_bbox(x=x, y=y), psf2.compute_stellar_bbox(x=x, y=y)) 

197 tc.assertEqual(psf1.compute_stellar_image(x=x, y=y), psf2.compute_stellar_image(x=x, y=y)) 

198 n_points_tested += 1 

199 return n_points_tested 

200 

201 

202def compare_image_to_legacy( 

203 tc: unittest.TestCase, image: Image, legacy_image: Any, expect_view: bool | None = None 

204) -> None: 

205 """Compare an `.Image` object to a legacy `lsst.afw.image.Image` object.""" 

206 tc.assertEqual(image.bbox, Box.from_legacy(legacy_image.getBBox())) 

207 if expect_view is not None: 

208 tc.assertEqual(np.may_share_memory(image.array, legacy_image.array), expect_view) 

209 if not expect_view: 

210 np.testing.assert_array_equal(image.array, legacy_image.array) 

211 

212 

213def compare_mask_to_legacy( 

214 tc: unittest.TestCase, mask: Mask, legacy_mask: Any, plane_map: Mapping[str, MaskPlane] | None = None 

215) -> None: 

216 """Compare a `.Mask` object to a legacy `lsst.afw.image.Mask` object.""" 

217 tc.assertEqual(mask.bbox, Box.from_legacy(legacy_mask.getBBox())) 

218 if plane_map is None: 

219 plane_map = {plane.name: plane for plane in mask.schema if plane is not None} 

220 for old_name, new_plane in plane_map.items(): 

221 np.testing.assert_array_equal( 

222 (legacy_mask.array & legacy_mask.getPlaneBitMask(old_name)).astype(bool), 

223 mask.get(new_plane.name), 

224 ) 

225 

226 

227def compare_masked_image_to_legacy( 

228 tc: unittest.TestCase, 

229 masked_image: MaskedImage, 

230 legacy_masked_image: Any, 

231 *, 

232 plane_map: Mapping[str, MaskPlane] | None = None, 

233 expect_view: bool | None = None, 

234 alternates: Mapping[str, Any] | None = None, 

235) -> None: 

236 """Compare a `.MaskedImage` object to a legacy `lsst.afw.image.MaskedImage` 

237 object. 

238 

239 Parameters 

240 ---------- 

241 tc 

242 Test case to use for asserts. 

243 masked_image 

244 New image to test. 

245 legacy_masked_image 

246 Legacy image to test against. 

247 plane_map 

248 Mapping between new and legacy mask planes. 

249 expect_view 

250 Whether to test that the image and variance arrays do or do not share 

251 memory. 

252 alternates 

253 A mapping of other versions of one or more (new) components to also 

254 check against the legacy versions of those components. 

255 """ 

256 compare_image_to_legacy(tc, masked_image.image, legacy_masked_image.getImage(), expect_view=expect_view) 

257 compare_mask_to_legacy(tc, masked_image.mask, legacy_masked_image.getMask(), plane_map=plane_map) 

258 compare_image_to_legacy( 

259 tc, masked_image.variance, legacy_masked_image.getVariance(), expect_view=expect_view 

260 ) 

261 if alternates: 

262 if image := alternates.get("image"): 

263 compare_image_to_legacy(tc, image, legacy_masked_image.getImage(), expect_view=expect_view) 

264 if mask := alternates.get("mask"): 

265 compare_mask_to_legacy(tc, mask, legacy_masked_image.getMask(), plane_map=plane_map) 

266 if variance := alternates.get("variance"): 

267 compare_image_to_legacy(tc, variance, legacy_masked_image.getVariance(), expect_view=expect_view) 

268 

269 

270def compare_visit_image_to_legacy( 

271 tc: unittest.TestCase, 

272 visit_image: VisitImage, 

273 legacy_exposure: Any, 

274 *, 

275 plane_map: Mapping[str, MaskPlane] | None = None, 

276 expect_view: bool | None = None, 

277 instrument: str, 

278 visit: int, 

279 detector: int, 

280 applied_legacy_photo_calib: LegacyPhotoCalib | None = None, 

281 alternates: Mapping[str, Any] | None = None, 

282) -> None: 

283 """Compare a `.VisitImage` object to a legacy `lsst.afw.image.Exposure` 

284 object. 

285 

286 Parameters 

287 ---------- 

288 tc 

289 Test case to use for asserts. 

290 visit_image 

291 New image to test. 

292 legacy_exposure 

293 Legacy image to test against. 

294 plane_map 

295 Mapping between new and legacy mask planes. 

296 expect_view 

297 Whether to test that the image and variance arrays do or do not share 

298 memory. 

299 instrument 

300 Expected instrument name. 

301 visit 

302 Expected visit ID. 

303 detector 

304 Expected detector ID. 

305 alternates 

306 A mapping of other versions of one or more (new) components to also 

307 check against the legacy versions of those components. 

308 """ 

309 compare_masked_image_to_legacy( 

310 tc, 

311 visit_image, 

312 legacy_exposure.getMaskedImage(), 

313 plane_map=plane_map, 

314 expect_view=expect_view, 

315 alternates=alternates, 

316 ) 

317 detector_bbox = Box.from_legacy(legacy_exposure.getDetector().getBBox()) 

318 compare_projection_to_legacy_wcs( 

319 tc, 

320 visit_image.projection, 

321 legacy_exposure.getWcs(), 

322 DetectorFrame(instrument=instrument, visit=visit, detector=detector, bbox=detector_bbox), 

323 visit_image.bbox, 

324 ) 

325 tc.assertIs(visit_image.projection, visit_image.mask.projection) 

326 tc.assertIs(visit_image.projection, visit_image.variance.projection) 

327 compare_psf_to_legacy(tc, visit_image.psf, legacy_exposure.getPsf()) 

328 compare_observation_summary_stats_to_legacy( 

329 tc, visit_image.summary_stats, legacy_exposure.info.getSummaryStats() 

330 ) 

331 compare_detector_to_legacy(tc, visit_image.detector, legacy_exposure.getDetector(), is_raw_assembled=True) 

332 # Make a tiny box for Field comparisons that need to make arrays; that can 

333 # get expensive otherwisre. 

334 tiny_bbox = detector_bbox.local[2:4, 3:6] 

335 compare_aperture_corrections_to_legacy( 

336 tc, visit_image.aperture_corrections, legacy_exposure.info.getApCorrMap(), tiny_bbox 

337 ) 

338 compare_photo_calib_to_legacy( 

339 tc, 

340 visit_image.photometric_scaling, 

341 legacy_exposure.info.getPhotoCalib(), 

342 applied_legacy_photo_calib=applied_legacy_photo_calib, 

343 subimage_bbox=tiny_bbox, 

344 ) 

345 if alternates: 

346 if (bbox := alternates.get("bbox")) is not None: 

347 tc.assertEqual(bbox, visit_image.bbox) 

348 if projection := alternates.get("projection"): 

349 compare_projection_to_legacy_wcs( 

350 tc, 

351 projection, 

352 legacy_exposure.getWcs(), 

353 DetectorFrame(instrument=instrument, visit=visit, detector=detector, bbox=detector_bbox), 

354 visit_image.bbox, 

355 ) 

356 if psf := alternates.get("psf"): 

357 compare_psf_to_legacy(tc, psf, legacy_exposure.getPsf()) 

358 if summary_stats := alternates.get("summary_stats"): 

359 compare_observation_summary_stats_to_legacy( 

360 tc, summary_stats, legacy_exposure.info.getSummaryStats() 

361 ) 

362 if detector_obj := alternates.get("detector"): 

363 compare_detector_to_legacy(tc, detector_obj, legacy_exposure.getDetector(), is_raw_assembled=True) 

364 if obs_info := alternates.get("obs_info"): 

365 visitInfo = legacy_exposure.visitInfo 

366 tc.assertEqual(obs_info.instrument, visitInfo.getInstrumentLabel()) 

367 if aperture_corrections := alternates.get("aperture_corrections"): 

368 compare_aperture_corrections_to_legacy( 

369 tc, aperture_corrections, legacy_exposure.info.getApCorrMap(), tiny_bbox 

370 ) 

371 if (photometric_scaling := alternates.get("photometic_scaling", ...)) is not ...: 

372 compare_photo_calib_to_legacy( 

373 tc, 

374 photometric_scaling, 

375 legacy_exposure.info.getPhotoCalib(), 

376 applied_legacy_photo_calib=applied_legacy_photo_calib, 

377 subimage_bbox=tiny_bbox, 

378 ) 

379 

380 

381def compare_photo_calib_to_legacy( 

382 tc: unittest.TestCase, 

383 photometric_scaling: BaseField | None, 

384 legacy_photo_calib: LegacyPhotoCalib, 

385 *, 

386 applied_legacy_photo_calib: LegacyPhotoCalib | None = None, 

387 subimage_bbox: Box, 

388) -> None: 

389 if legacy_photo_calib._isConstant: 

390 if legacy_photo_calib.getCalibrationMean() == 1.0: 

391 if applied_legacy_photo_calib is None: 

392 tc.assertIsNone(photometric_scaling) 

393 return 

394 else: 

395 legacy_photo_calib = applied_legacy_photo_calib 

396 if legacy_photo_calib._isConstant: 

397 assert isinstance(photometric_scaling, ChebyshevField) 

398 assert_close( 

399 tc, photometric_scaling.coefficients, np.array([[legacy_photo_calib.getCalibrationMean()]]) 

400 ) 

401 else: 

402 assert photometric_scaling is not None 

403 compare_field_to_legacy( 

404 tc, 

405 photometric_scaling / legacy_photo_calib.getCalibrationMean(), 

406 legacy_photo_calib.computeScaledCalibration(), 

407 subimage_bbox, 

408 ) 

409 

410 

411def compare_cell_coadd_to_legacy( 

412 tc: unittest.TestCase, 

413 cell_coadd: CellCoadd, 

414 legacy_cell_coadd: MultipleCellCoadd, 

415 *, 

416 tract_bbox: Box, 

417 plane_map: Mapping[str, MaskPlane] | None = None, 

418 alternates: Mapping[str, Any] | None = None, 

419 psf_points: XY[np.ndarray] | YX[np.ndarray] | None = None, 

420) -> None: 

421 """Compare a `.cells.CellCoadd` object to a legacy 

422 `lsst.cell_coadds.MultipleCellCoadd` object. 

423 

424 Parameters 

425 ---------- 

426 tc 

427 Test case to use for asserts. 

428 cell_coadd 

429 New coadd to test. 

430 legacy_cell_coadd 

431 Legacy coadd to test against. 

432 tract_bbox 

433 Bounding box of the full tract. 

434 psf_points 

435 Points to use to compare the PSFs. 

436 plane_map 

437 Mapping between new and legacy mask planes. 

438 alternates 

439 A mapping of other versions of one or more (new) components to also 

440 check against the legacy versions of those components. 

441 """ 

442 legacy_stitched = legacy_cell_coadd.stitch(cell_coadd.bbox.to_legacy()) 

443 compare_image_to_legacy(tc, cell_coadd.image, legacy_stitched.image, expect_view=False) 

444 compare_mask_to_legacy(tc, cell_coadd.mask, legacy_stitched.mask, plane_map=plane_map) 

445 compare_image_to_legacy(tc, cell_coadd.variance, legacy_stitched.variance, expect_view=False) 

446 if legacy_stitched.mask_fractions is not None: 

447 compare_image_to_legacy( 

448 tc, cell_coadd.mask_fractions["rejected"], legacy_stitched.mask_fractions, expect_view=False 

449 ) 

450 for n in range(legacy_stitched.n_noise_realizations): 

451 compare_image_to_legacy( 

452 tc, cell_coadd.noise_realizations[n], legacy_stitched.noise_realizations[n], expect_view=False 

453 ) 

454 tc.assertEqual(cell_coadd.skymap, legacy_stitched.identifiers.skymap) 

455 tc.assertEqual(cell_coadd.tract, legacy_stitched.identifiers.tract) 

456 tc.assertEqual(cell_coadd.patch.index.x, legacy_stitched.identifiers.patch.x) 

457 tc.assertEqual(cell_coadd.patch.index.y, legacy_stitched.identifiers.patch.y) 

458 tc.assertEqual(cell_coadd.band, legacy_stitched.identifiers.band) 

459 tc.assertTrue(tract_bbox.contains(cell_coadd.patch.outer_bbox)) 

460 tc.assertTrue(cell_coadd.patch.outer_bbox.contains(cell_coadd.patch.inner_bbox)) 

461 tc.assertTrue(cell_coadd.patch.outer_bbox.contains(cell_coadd.bbox)) 

462 tc.assertEqual(cell_coadd.unit, u.Unit(legacy_cell_coadd.common.units.value)) 

463 tc.assertTrue(cell_coadd.bounds.bbox.contains(cell_coadd.bbox)) 

464 tc.assertTrue(cell_coadd.grid.bbox.contains(cell_coadd.bbox)) 

465 compare_projection_to_legacy_wcs( 

466 tc, 

467 cell_coadd.projection, 

468 legacy_cell_coadd.wcs, 

469 TractFrame( 

470 skymap=legacy_cell_coadd.identifiers.skymap, 

471 tract=legacy_cell_coadd.identifiers.tract, 

472 bbox=tract_bbox, 

473 ), 

474 cell_coadd.bbox, 

475 is_fits=True, 

476 ) 

477 tc.assertIs(cell_coadd.projection, cell_coadd.mask.projection) 

478 tc.assertIs(cell_coadd.projection, cell_coadd.variance.projection) 

479 compare_psf_to_legacy( 

480 tc, cell_coadd.psf, legacy_stitched.psf, expect_legacy_raise_on_out_of_bounds=True, points=psf_points 

481 ) 

482 compare_cell_coadd_provenance_to_legacy(tc, cell_coadd.provenance, legacy_cell_coadd) 

483 if alternates: 

484 if projection := alternates.get("projection"): 

485 compare_projection_to_legacy_wcs( 

486 tc, 

487 projection, 

488 legacy_stitched.wcs, 

489 TractFrame( 

490 skymap=legacy_cell_coadd.identifiers.skymap, 

491 tract=legacy_cell_coadd.identifiers.tract, 

492 bbox=tract_bbox, 

493 ), 

494 cell_coadd.bbox, 

495 is_fits=True, 

496 ) 

497 if psf := alternates.get("psf"): 

498 compare_psf_to_legacy(tc, psf, legacy_stitched.psf, points=psf_points) 

499 if provenance := alternates.get("provenance"): 

500 compare_cell_coadd_provenance_to_legacy(tc, provenance, legacy_cell_coadd) 

501 

502 

503def compare_cell_coadd_provenance_to_legacy( 

504 tc: unittest.TestCase, provenance: CoaddProvenance, legacy_cell_coadd: MultipleCellCoadd 

505) -> None: 

506 """Compare a `.cells.CoaddProvenance` object to a legacy 

507 `lsst.cell_coadds.MultipleCellCoadd` object. 

508 

509 Parameters 

510 ---------- 

511 tc 

512 Test case to use for asserts. 

513 provenance 

514 New provenance object to test. 

515 legacy_cell_coadd 

516 Legacy coadd to test against. 

517 """ 

518 from lsst.cell_coadds import ObservationIdentifiers 

519 

520 for legacy_cell in legacy_cell_coadd.cells.values(): 

521 cell_index = CellIJ.from_legacy(legacy_cell.identifiers.cell) 

522 prov = provenance[cell_index] 

523 legacy_table = astropy.table.Table( 

524 rows=[ 

525 [ 

526 ids.instrument, 

527 ids.visit, 

528 ids.detector, 

529 ids.day_obs, 

530 ids.physical_filter, 

531 legacy_input.overlaps_center, 

532 legacy_input.overlap_fraction, 

533 legacy_input.weight, 

534 legacy_input.psf_shape.getIxx(), 

535 legacy_input.psf_shape.getIyy(), 

536 legacy_input.psf_shape.getIxy(), 

537 legacy_input.psf_shape_flag, 

538 ] 

539 for ids, legacy_input in legacy_cell.inputs.items() 

540 ], 

541 dtype=[ 

542 np.object_, 

543 np.uint64, 

544 np.uint16, 

545 np.uint32, 

546 np.object_, 

547 np.bool_, 

548 np.float64, 

549 np.float64, 

550 np.float64, 

551 np.float64, 

552 np.float64, 

553 np.bool_, 

554 ], 

555 names=[ 

556 "instrument", 

557 "visit", 

558 "detector", 

559 "day_obs", 

560 "physical_filter", 

561 "overlaps_center", 

562 "overlap_fraction", 

563 "weight", 

564 "psf_shape_xx", 

565 "psf_shape_yy", 

566 "psf_shape_xy", 

567 "psf_shape_flag", 

568 ], 

569 ) 

570 # For a single cell all 'inputs' are also 'contributions'. 

571 tc.assertEqual(len(legacy_cell.inputs), len(prov.inputs)) 

572 tc.assertEqual(len(legacy_cell.inputs), len(prov.contributions)) 

573 prov.inputs.sort(["instrument", "visit", "detector"]) 

574 prov.contributions.sort(["instrument", "visit", "detector"]) 

575 legacy_table.sort(["instrument", "visit", "detector"]) 

576 np.testing.assert_array_equal(prov.inputs["instrument"], prov.contributions["instrument"]) 

577 np.testing.assert_array_equal(prov.inputs["visit"], prov.contributions["visit"]) 

578 np.testing.assert_array_equal(prov.inputs["detector"], prov.contributions["detector"]) 

579 np.testing.assert_array_equal(prov.inputs["instrument"], legacy_table["instrument"]) 

580 np.testing.assert_array_equal(prov.inputs["visit"], legacy_table["visit"]) 

581 np.testing.assert_array_equal(prov.inputs["detector"], legacy_table["detector"]) 

582 np.testing.assert_array_equal(prov.inputs["physical_filter"], legacy_table["physical_filter"]) 

583 np.testing.assert_array_equal(prov.inputs["day_obs"], legacy_table["day_obs"]) 

584 np.testing.assert_array_equal(prov.contributions["overlaps_center"], legacy_table["overlaps_center"]) 

585 np.testing.assert_array_equal( 

586 prov.contributions["overlap_fraction"], legacy_table["overlap_fraction"] 

587 ) 

588 np.testing.assert_array_equal(prov.contributions["weight"], legacy_table["weight"]) 

589 np.testing.assert_array_equal(prov.contributions["psf_shape_xx"], legacy_table["psf_shape_xx"]) 

590 np.testing.assert_array_equal(prov.contributions["psf_shape_yy"], legacy_table["psf_shape_yy"]) 

591 np.testing.assert_array_equal(prov.contributions["psf_shape_xy"], legacy_table["psf_shape_xy"]) 

592 np.testing.assert_array_equal(prov.contributions["psf_shape_flag"], legacy_table["psf_shape_flag"]) 

593 for row in prov.inputs: 

594 polygon_key = ObservationIdentifiers(**{k: row[k] for k in row.keys() if k != "polygon"}) 

595 legacy_polygon = legacy_cell_coadd.common.visit_polygons[polygon_key] 

596 tc.assertEqual(legacy_polygon, row["polygon"].to_legacy()) 

597 

598 

599def compare_psf_to_legacy( 

600 tc: unittest.TestCase, 

601 psf: PointSpreadFunction, 

602 legacy_psf: Any, 

603 points: YX[np.ndarray] | XY[np.ndarray] | None = None, 

604 expect_legacy_raise_on_out_of_bounds: bool = False, 

605) -> int: 

606 """Compare a PSF model object to its legacy interface. 

607 

608 Parameters 

609 ---------- 

610 tc 

611 Test case object with assert methods to use. 

612 psf 

613 Point-spread function to test. 

614 legacy_psf 

615 Legacy `lsst.afw.detection.Psf` instance to compare with. 

616 points 

617 Points to evaluate the PSFs at. If not provided, the intersection of 

618 the PSF bounding boxes are used to generate points on a grid. 

619 expect_legacy_raise_on_out_of_bounds 

620 If `True`, expect ``legacy_psf`` to raise 

621 `lsst.afw.detection.InvalidPsfError` when evaluated at a position 

622 considered out-of-bounds by ``psf``. 

623 

624 Returns 

625 ------- 

626 `int` 

627 The number of points actually tested. 

628 """ 

629 from lsst.afw.detection import InvalidPsfError 

630 

631 if points is None: 

632 points = psf.bounds.bbox.meshgrid(n=3).map(np.ravel) 

633 legacy_points = arrays_to_legacy_points(points.x, points.y) 

634 n_points_tested: int = 0 

635 for p in legacy_points: 

636 if not psf.bounds.contains(x=p.x, y=p.y): 

637 if expect_legacy_raise_on_out_of_bounds: 

638 with tc.assertRaises(InvalidPsfError): 

639 legacy_psf.computeKernelImage(p) 

640 continue 

641 tc.assertEqual(psf.kernel_bbox, Box.from_legacy(legacy_psf.computeKernelBBox(p))) 

642 tc.assertEqual( 

643 psf.compute_kernel_image(x=p.x, y=p.y), Image.from_legacy(legacy_psf.computeKernelImage(p)) 

644 ) 

645 tc.assertEqual( 

646 psf.compute_stellar_bbox(x=p.x, y=p.y), Box.from_legacy(legacy_psf.computeImageBBox(p)) 

647 ) 

648 tc.assertEqual(psf.compute_stellar_image(x=p.x, y=p.y), Image.from_legacy(legacy_psf.computeImage(p))) 

649 n_points_tested += 1 

650 return n_points_tested 

651 

652 

653def compare_field_to_legacy( 

654 tc: unittest.TestCase, 

655 field: BaseField, 

656 legacy_field: Any, 

657 subimage_bbox: Box, 

658) -> None: 

659 """Test a Field object by comparing it to an equivalent 

660 `lsst.afw.math.BoundedField`. 

661 

662 Parameters 

663 ---------- 

664 tc 

665 Test case object with assert methods to use. 

666 field 

667 Field to test. 

668 legacy_field : ``lsst.afw.math.BoundedField`` 

669 Equivalent legacy bounded field. 

670 subimage_bbox 

671 Bounding box for full-image tests. 

672 """ 

673 tc.assertEqual(field.bounds.bbox, Box.from_legacy(legacy_field.getBBox())) 

674 # Pixel coordinates to test the numpy array interface with. 

675 pixel_xy = field.bounds.bbox.meshgrid(n=5).map(np.ravel) 

676 assert_close(tc, field(x=pixel_xy.x, y=pixel_xy.y), legacy_field.evaluate(pixel_xy.x, pixel_xy.y)) 

677 legacy_image_1 = Image(0, bbox=subimage_bbox, dtype=np.float64).to_legacy() 

678 legacy_field.addToImage(legacy_image_1, overlapOnly=True) 

679 assert_images_equal( 

680 tc, field.render(subimage_bbox), Image.from_legacy(legacy_image_1, unit=field.unit), rtol=1e-13 

681 ) 

682 

683 

684def compare_aperture_corrections_to_legacy( 

685 tc: unittest.TestCase, 

686 aperture_corrections: ApertureCorrectionMap, 

687 legacy_ap_corr_map: Any, 

688 subimage_bbox: Box, 

689) -> None: 

690 """Test an aperture correction `dict` by comparing it to an equivalent 

691 `lsst.afw.image.ApCorrMap`. 

692 

693 Parameters 

694 ---------- 

695 tc 

696 Test case object with assert methods to use. 

697 aperture_corrections 

698 Dictionary to test. 

699 legacy_ap_corr_map : ``lsst.afw.image.ApCorrMap`` 

700 Equivalent legacy aperture correction map. 

701 subimage_bbox 

702 Bounding box for full-image tests. 

703 """ 

704 tc.assertEqual(aperture_corrections.keys(), set(legacy_ap_corr_map.keys())) 

705 for name, field in aperture_corrections.items(): 

706 compare_field_to_legacy(tc, field, legacy_ap_corr_map[name], subimage_bbox) 

707 

708 

709def compare_observation_summary_stats_to_legacy( 

710 tc: unittest.TestCase, 

711 summary_stats: ObservationSummaryStats, 

712 legacy_summary_stats: Any, 

713) -> None: 

714 """Test an ObservationSummaryStats object by comparing it to an equivalent 

715 `lsst.afw.image.ExposureSummaryStats`. 

716 

717 Parameters 

718 ---------- 

719 tc 

720 Test case object with assert methods to use. 

721 summary_stats 

722 Struct to test. 

723 legacy : ``lsst.afw.image.ExposureSummaryStats`` 

724 Equivalent legacy struct. 

725 """ 

726 for field in dataclasses.fields(legacy_summary_stats): 

727 a = getattr(legacy_summary_stats, field.name) 

728 b = getattr(summary_stats, field.name) 

729 if isinstance(b, tuple): 

730 for ai, bi in zip(a, b): 

731 tc.assertTrue(ai == bi or (math.isnan(ai) and math.isnan(bi)), f"{field.name}: {a} != {b}") 

732 else: 

733 tc.assertTrue(a == b or (math.isnan(a) and math.isnan(b)), f"{field.name}: {a} != {b}") 

734 

735 

736def compare_projection_to_legacy_wcs[F: Frame]( 

737 tc: unittest.TestCase, 

738 projection: Projection[F], 

739 legacy_wcs: Any, 

740 pixel_frame: F, 

741 subimage_bbox: Box, 

742 is_fits: bool = False, 

743) -> None: 

744 """Test a Projection object by comparing it to an equivalent 

745 `lsst.afw.geom.SkyWcs`. 

746 

747 Parameters 

748 ---------- 

749 tc 

750 Test case object with assert methods to use. 

751 projection 

752 Projection to test. 

753 legacy_wcs : ``lsst.afw.geom.SkyWcs`` 

754 Equivalent legacy WCS. 

755 pixel_frame 

756 Expected pixel frame for the projection. 

757 subimage_bbox 

758 Bounding box of points to generate for tests. 

759 is_fits 

760 Whether this projection is expected to be exactly representable as a 

761 FIT WCS. If `False` it is assumed to have a FITS approximation 

762 attached instead. 

763 """ 

764 # Pixel coordinates to test on over the subimage region of interest: 

765 pixel_xy = subimage_bbox.meshgrid(step=50).map(np.ravel) 

766 # Array indices of those pixel values (subtract off bbox starts): 

767 subimage_array_xy = XY(x=pixel_xy.x - subimage_bbox.x.start, y=pixel_xy.y - subimage_bbox.y.start) 

768 sky_coords = legacy_coords_to_astropy( 

769 legacy_wcs.pixelToSky(arrays_to_legacy_points(pixel_xy.x, pixel_xy.y)) 

770 ) 

771 # Test transforming with the Projection itself, which also tests its 

772 # nested Transform and an Astropy High-Level WCS view with no origin 

773 # change. 

774 check_projection(tc, projection, pixel_xy, sky_coords, pixel_frame) 

775 # Also test the Astropy High-Level WCS view with an origin change to 

776 # array indices. 

777 check_astropy_wcs_interface( 

778 tc, projection.as_astropy(subimage_bbox), subimage_array_xy, sky_coords, pixel_atol=1e-5 

779 ) 

780 if is_fits: 

781 fits_wcs = projection.as_fits_wcs(subimage_bbox, allow_approximation=True) 

782 assert fits_wcs is not None 

783 check_astropy_wcs_interface(tc, fits_wcs, subimage_array_xy, sky_coords, pixel_atol=1e-5) 

784 # Use that FITS approximation to check that we can make a 

785 # Projection from a FITS WCS, too. 

786 fits_projection = Projection.from_fits_wcs(fits_wcs, pixel_frame) 

787 check_projection( 

788 tc, 

789 fits_projection, 

790 subimage_array_xy, 

791 sky_coords, 

792 pixel_frame, 

793 pixel_atol=1e-5, 

794 ) 

795 # We want Projections we create from a FITS WCS to be backed by an 

796 # AST FrameSet so we can convert them into legacy 

797 # `lsst.afw.geom.SkyWcs` objects if desired. 

798 tc.assertIn("Begin FrameSet", fits_projection.show()) 

799 else: 

800 tc.assertIsNone(projection.as_fits_wcs(subimage_bbox, allow_approximation=False)) 

801 # The legacy SkyWcs should instead have a FITS approximation 

802 # attached; run the same tests on that. 

803 assert projection.fits_approximation is not None 

804 compare_projection_to_legacy_wcs( 

805 tc, 

806 projection.fits_approximation, 

807 legacy_wcs.getFitsApproximation(), 

808 pixel_frame, 

809 subimage_bbox, 

810 is_fits=True, 

811 ) 

812 

813 

814def check_transform[I: Frame, O: Frame]( 

815 tc: unittest.TestCase, 

816 transform: Transform[I, O], 

817 input_xy: XY[np.ndarray], 

818 output_xy: XY[np.ndarray], 

819 in_frame: Frame, 

820 out_frame: Frame, 

821 *, 

822 check_inverted: bool = True, 

823 in_atol: u.Quantity | None = None, 

824 out_atol: u.Quantity | None = None, 

825) -> None: 

826 """Test Transform against known arrays of input and output points. 

827 

828 Parameters 

829 ---------- 

830 tc 

831 Test case object with assert methods to use. 

832 transform 

833 Transform to test. 

834 input_xy 

835 Arrays of input points. 

836 output_xy 

837 Arrays of output points. 

838 in_frame 

839 Expected input frame. 

840 out_frame 

841 Expected output frame. 

842 check_inverted 

843 If `True`, recurse (once) to test the inverse transform. 

844 in_atol 

845 Expected absolute precision of input points. 

846 out_atol 

847 Expected absolute precision of output points. 

848 """ 

849 tc.assertEqual(transform.in_frame, in_frame) 

850 tc.assertEqual(transform.out_frame, out_frame) 

851 in_atol_v = in_atol.to_value(in_frame.unit) if in_atol is not None else None 

852 out_atol_v = out_atol.to_value(out_frame.unit) if out_atol is not None else None 

853 # Test array interfaces. 

854 test_output_xy = transform.apply_forward(x=input_xy.x, y=input_xy.y) 

855 assert_close(tc, test_output_xy.x, output_xy.x, atol=out_atol_v) 

856 assert_close(tc, test_output_xy.y, output_xy.y, atol=out_atol_v) 

857 test_input_xy = transform.apply_inverse(x=output_xy.x, y=output_xy.y) 

858 assert_close(tc, test_input_xy.x, input_xy.x, atol=in_atol_v) 

859 assert_close(tc, test_input_xy.y, input_xy.y, atol=in_atol_v) 

860 # Test scalar interfaces with numpy scalars. 

861 for input_x, input_y, output_x, output_y in zip(input_xy.x, input_xy.y, output_xy.x, output_xy.y): 

862 assert_close(tc, transform.apply_forward(x=input_x, y=input_y).x, output_x, atol=out_atol_v) 

863 assert_close(tc, transform.apply_forward(x=input_x, y=input_y).y, output_y, atol=out_atol_v) 

864 assert_close(tc, transform.apply_inverse(x=output_x, y=output_y).x, input_x, atol=in_atol_v) 

865 assert_close(tc, transform.apply_inverse(x=output_x, y=output_y).y, input_y, atol=in_atol_v) 

866 # Test quantity array interfaces. 

867 input_q_xy = XY(x=input_xy.x * transform.in_frame.unit, y=input_xy.y * transform.in_frame.unit) 

868 output_q_xy = XY(x=output_xy.x * transform.out_frame.unit, y=output_xy.y * transform.out_frame.unit) 

869 test_output_q_xy = transform.apply_forward_q(x=input_q_xy.x, y=input_q_xy.y) 

870 assert_close(tc, test_output_q_xy.x, output_q_xy.x, atol=out_atol) 

871 assert_close(tc, test_output_q_xy.y, output_q_xy.y, atol=out_atol) 

872 test_input_q_xy = transform.apply_inverse_q(x=output_q_xy.x, y=output_q_xy.y) 

873 assert_close(tc, test_input_q_xy.x, input_q_xy.x, atol=in_atol) 

874 assert_close(tc, test_input_q_xy.y, input_q_xy.y, atol=in_atol) 

875 # Test quantity scalar interfaces. 

876 for input_q_x, input_q_y, output_q_x, output_q_y in zip( 

877 input_q_xy.x, input_q_xy.y, output_q_xy.x, output_q_xy.y 

878 ): 

879 assert_close(tc, transform.apply_forward_q(x=input_q_x, y=input_q_y).x, output_q_x, atol=out_atol) 

880 assert_close(tc, transform.apply_forward_q(x=input_q_x, y=input_q_y).y, output_q_y, atol=out_atol) 

881 assert_close(tc, transform.apply_inverse_q(x=output_q_x, y=output_q_y).x, input_q_x, atol=in_atol) 

882 assert_close(tc, transform.apply_inverse_q(x=output_q_x, y=output_q_y).y, input_q_y, atol=in_atol) 

883 if check_inverted: 

884 # Test the inverse transform. 

885 check_transform( 

886 tc, 

887 transform.inverted(), 

888 output_xy, 

889 input_xy, 

890 out_frame, 

891 in_frame, 

892 check_inverted=False, 

893 out_atol=in_atol, 

894 in_atol=out_atol, 

895 ) 

896 

897 

898def check_projection[P: Frame]( 

899 tc: unittest.TestCase, 

900 projection: Projection[P], 

901 pixel_xy: XY[np.ndarray], 

902 sky_coords: SkyCoord, 

903 pixel_frame: Frame, 

904 *, 

905 pixel_atol: float | None = None, 

906 sky_atol: u.Quantity | None = None, 

907) -> None: 

908 """Test a `.Projection` instance against known arrays of pixel and sky 

909 coordinates. 

910 

911 Parameters 

912 ---------- 

913 tc 

914 Test case object with assert methods to use. 

915 projection 

916 Projection to test. 

917 pixel_xy 

918 Arrays of pixel coordinates. 

919 sky_coords 

920 Corresponding sky coordinates. 

921 pixel_frame 

922 Expected pixel frame. 

923 pixel_atol 

924 Expected absolute precision of pixel points. 

925 sky_atol 

926 Expected absolute precision of sky coordinates. 

927 """ 

928 tc.assertEqual(projection.pixel_frame, pixel_frame) 

929 tc.assertEqual(projection.sky_frame, SkyFrame.ICRS) 

930 sky_atol_v = sky_atol.to_value(SkyFrame.ICRS.unit) if sky_atol is not None else None 

931 pixel_atol_q = pixel_atol * u.pix if pixel_atol is not None else None 

932 # Test array interfaces. 

933 test_pixel_xy = cast(XY[np.ndarray], projection.sky_to_pixel(sky_coords)) 

934 assert_close(tc, test_pixel_xy.x, pixel_xy.x, atol=pixel_atol) 

935 assert_close(tc, test_pixel_xy.y, pixel_xy.y, atol=pixel_atol) 

936 test_sky_astropy = projection.pixel_to_sky(x=pixel_xy.x, y=pixel_xy.y) 

937 assert_close(tc, test_sky_astropy.ra, sky_coords.ra, atol=sky_atol_v) 

938 assert_close(tc, test_sky_astropy.dec, sky_coords.dec, atol=sky_atol_v) 

939 # Test scalar interfaces. 

940 for pixel_x, pixel_y, sky_single in zip(pixel_xy.x, pixel_xy.y, sky_coords): 

941 assert_close(tc, projection.sky_to_pixel(sky_single).x, pixel_x, atol=pixel_atol) 

942 assert_close(tc, projection.sky_to_pixel(sky_single).y, pixel_y, atol=pixel_atol) 

943 assert_close(tc, projection.pixel_to_sky(x=pixel_x, y=pixel_y).ra, sky_single.ra, atol=sky_atol_v) 

944 assert_close(tc, projection.pixel_to_sky(x=pixel_x, y=pixel_y).dec, sky_single.dec, atol=sky_atol_v) 

945 # Test the underlying Transform object. 

946 sky_xy = XY(x=sky_coords.ra.to_value(u.rad), y=sky_coords.dec.to_value(u.rad)) 

947 check_transform( 

948 tc, 

949 projection.pixel_to_sky_transform, 

950 pixel_xy, 

951 sky_xy, 

952 pixel_frame, 

953 SkyFrame.ICRS, 

954 check_inverted=False, 

955 in_atol=pixel_atol_q, 

956 out_atol=sky_atol, 

957 ) 

958 check_transform( 

959 tc, 

960 projection.sky_to_pixel_transform, 

961 sky_xy, 

962 pixel_xy, 

963 SkyFrame.ICRS, 

964 pixel_frame, 

965 check_inverted=False, 

966 in_atol=sky_atol, 

967 out_atol=pixel_atol_q, 

968 ) 

969 # Test the Astropy interface adapter. 

970 check_astropy_wcs_interface( 

971 tc, projection.as_astropy(), pixel_xy, sky_coords, pixel_atol=pixel_atol, sky_atol=sky_atol 

972 ) 

973 

974 

975def assert_projections_equal( 

976 tc: unittest.TestCase, 

977 a: Projection[Any] | None, 

978 b: Projection[Any] | None, 

979 expect_identity: bool | None = None, 

980) -> None: 

981 """Test that two `.Projection` instances are equivalent.""" 

982 if a is None and b is None: 

983 return 

984 assert a is not None and b is not None 

985 match expect_identity: 

986 case True: 

987 tc.assertIs(a, b) 

988 return 

989 case False: 

990 tc.assertIsNot(a, b) 

991 case None if a is b: 

992 return 

993 tc.assertEqual(a.pixel_frame, b.pixel_frame) 

994 tc.assertEqual(a.show(simplified=True), b.show(simplified=True)) 

995 assert_projections_equal( 

996 tc, a.fits_approximation, cast(Projection[Any], b.fits_approximation), expect_identity=False 

997 ) 

998 

999 

1000def check_astropy_wcs_interface( 

1001 tc: unittest.TestCase, 

1002 wcs: astropy.wcs.wcsapi.BaseHighLevelWCS, 

1003 pixel_xy: XY[np.ndarray], 

1004 sky_coords: SkyCoord, 

1005 *, 

1006 pixel_atol: float | None = None, 

1007 sky_atol: u.Quantity | None = None, 

1008) -> None: 

1009 """Test an Astropy WCS instance against known arrays of pixel and 

1010 sky coordinates. 

1011 

1012 Parameters 

1013 ---------- 

1014 tc 

1015 Test case object with assert methods to use. 

1016 wcs 

1017 Astropy WCS object to test. 

1018 pixel_xy 

1019 Arrays of pixel coordinates. 

1020 sky_coords 

1021 Corresponding sky coordinates. 

1022 pixel_atol 

1023 Expected absolute precision of pixel points. 

1024 sky_atol 

1025 Expected absolute precision of sky coordinates. 

1026 """ 

1027 test_x, test_y = wcs.world_to_pixel(sky_coords) 

1028 assert_close(tc, test_x, pixel_xy.x, atol=pixel_atol) 

1029 assert_close(tc, test_y, pixel_xy.y, atol=pixel_atol) 

1030 test_sky_coords = wcs.pixel_to_world(pixel_xy.x, pixel_xy.y) 

1031 assert_close(tc, test_sky_coords.ra, sky_coords.ra, atol=sky_atol) 

1032 assert_close(tc, test_sky_coords.dec, sky_coords.dec, atol=sky_atol) 

1033 

1034 

1035def legacy_points_to_xy_array(legacy_points: list[Any]) -> XY[np.ndarray]: 

1036 """Convert a list of ``lsst.geom.Point2D`` objects to an `.XY` array.""" 

1037 return XY(x=np.array([p.x for p in legacy_points]), y=np.array([p.y for p in legacy_points])) 

1038 

1039 

1040def legacy_coords_to_astropy(legacy_coords: list[Any]) -> SkyCoord: 

1041 """Convert a list of ``lsst.geom.SpherePoint`` objects to an Astropy 

1042 coordinate object. 

1043 """ 

1044 return SkyCoord( 

1045 ra=np.array([p.getRa().asRadians() for p in legacy_coords]) * u.rad, 

1046 dec=np.array([p.getDec().asRadians() for p in legacy_coords]) * u.rad, 

1047 ) 

1048 

1049 

1050def arrays_to_legacy_points(x: np.ndarray, y: np.ndarray) -> list[Any]: 

1051 """Convert arrays of ``x`` and ``y`` to a list of ``lsst.geom.Point2D``.""" 

1052 from lsst.geom import Point2D 

1053 

1054 return [Point2D(x=xv, y=yv) for xv, yv in zip(x, y)] 

1055 

1056 

1057def compare_amplifier_to_legacy( 

1058 tc: unittest.TestCase, 

1059 amplifier: Amplifier, 

1060 legacy_amplifier: Any, 

1061 *, 

1062 is_raw_assembled: bool, 

1063 expect_nominal_calibrations: bool = True, 

1064) -> None: 

1065 """Compare an `~.cameras.Amplifier` to a legacy 

1066 `lsst.afw.cameraGeom.Amplifier`. 

1067 """ 

1068 tc.assertEqual(legacy_amplifier.getName(), amplifier.name) 

1069 tc.assertEqual(Box.from_legacy(legacy_amplifier.getBBox()), amplifier.bbox) 

1070 if is_raw_assembled: 

1071 raw_geom = amplifier.assembled_raw_geometry 

1072 else: 

1073 raw_geom = amplifier.unassembled_raw_geometry 

1074 assert raw_geom is not None 

1075 tc.assertEqual(ReadoutCorner.from_legacy(legacy_amplifier.getReadoutCorner()), raw_geom.readout_corner) 

1076 tc.assertEqual(Box.from_legacy(legacy_amplifier.getRawBBox()), raw_geom.bbox) 

1077 tc.assertEqual(Box.from_legacy(legacy_amplifier.getRawDataBBox()), raw_geom.data_bbox) 

1078 tc.assertEqual(legacy_amplifier.getRawFlipX(), raw_geom.flip_x) 

1079 tc.assertEqual(legacy_amplifier.getRawFlipY(), raw_geom.flip_y) 

1080 tc.assertEqual(legacy_amplifier.getRawXYOffset().getX(), raw_geom.x_offset) 

1081 tc.assertEqual(legacy_amplifier.getRawXYOffset().getY(), raw_geom.y_offset) 

1082 tc.assertEqual( 

1083 Box.from_legacy(legacy_amplifier.getRawHorizontalOverscanBBox()), raw_geom.horizontal_overscan_bbox 

1084 ) 

1085 tc.assertEqual( 

1086 Box.from_legacy(legacy_amplifier.getRawVerticalOverscanBBox()), raw_geom.vertical_overscan_bbox 

1087 ) 

1088 tc.assertEqual(Box.from_legacy(legacy_amplifier.getRawPrescanBBox()), raw_geom.horizontal_prescan_bbox) 

1089 if expect_nominal_calibrations: 

1090 assert amplifier.nominal_calibrations is not None 

1091 assert_equal_allow_nan(tc, legacy_amplifier.getGain(), amplifier.nominal_calibrations.gain) 

1092 assert_equal_allow_nan(tc, legacy_amplifier.getReadNoise(), amplifier.nominal_calibrations.read_noise) 

1093 assert_equal_allow_nan( 

1094 tc, legacy_amplifier.getSaturation(), amplifier.nominal_calibrations.saturation 

1095 ) 

1096 assert_equal_allow_nan( 

1097 tc, legacy_amplifier.getSuspectLevel(), amplifier.nominal_calibrations.suspect_level 

1098 ) 

1099 np.testing.assert_array_equal( 

1100 legacy_amplifier.getLinearityCoeffs(), amplifier.nominal_calibrations.linearity_coefficients 

1101 ) 

1102 tc.assertEqual(legacy_amplifier.getLinearityType(), amplifier.nominal_calibrations.linearity_type) 

1103 

1104 

1105def compare_detector_to_legacy( 

1106 tc: unittest.TestCase, 

1107 detector: Detector, 

1108 legacy_detector: Any, 

1109 *, 

1110 is_raw_assembled: bool, 

1111 expect_nominal_calibrations: bool = True, 

1112) -> None: 

1113 """Compare a `~.cameras.Detector` to a `lsst.afw.cameraGeom.Detector`.""" 

1114 from lsst.afw.cameraGeom import FIELD_ANGLE, FOCAL_PLANE, PIXELS 

1115 

1116 tc.assertEqual(legacy_detector.getName(), detector.name) 

1117 tc.assertEqual(legacy_detector.getId(), detector.id) 

1118 tc.assertEqual(DetectorType.from_legacy(legacy_detector.getType()), detector.type) 

1119 tc.assertEqual(Box.from_legacy(legacy_detector.getBBox()), detector.bbox) 

1120 tc.assertEqual(legacy_detector.getSerial(), detector.serial) 

1121 legacy_orientation = legacy_detector.getOrientation() 

1122 tc.assertEqual(legacy_orientation.getFpPosition3().getX(), detector.orientation.focal_plane_x) 

1123 tc.assertEqual(legacy_orientation.getFpPosition3().getY(), detector.orientation.focal_plane_y) 

1124 tc.assertEqual(legacy_orientation.getFpPosition3().getZ(), detector.orientation.focal_plane_z) 

1125 tc.assertEqual(legacy_orientation.getReferencePoint().getX(), detector.orientation.pixel_reference_x) 

1126 tc.assertEqual(legacy_orientation.getReferencePoint().getY(), detector.orientation.pixel_reference_y) 

1127 tc.assertEqual(legacy_orientation.getYaw().asRadians(), detector.orientation.yaw.to_value(u.rad)) 

1128 tc.assertEqual(legacy_orientation.getPitch().asRadians(), detector.orientation.pitch.to_value(u.rad)) 

1129 tc.assertEqual(legacy_orientation.getRoll().asRadians(), detector.orientation.roll.to_value(u.rad)) 

1130 tc.assertEqual(legacy_detector.getPixelSize().getX(), detector.pixel_size) 

1131 tc.assertEqual(legacy_detector.getPhysicalType(), detector.physical_type) 

1132 for amplifier, legacy_amplifier in zip(detector.amplifiers, legacy_detector.getAmplifiers(), strict=True): 

1133 compare_amplifier_to_legacy( 

1134 tc, 

1135 amplifier, 

1136 legacy_amplifier, 

1137 is_raw_assembled=is_raw_assembled, 

1138 expect_nominal_calibrations=expect_nominal_calibrations, 

1139 ) 

1140 pixel_xy = detector.bbox.meshgrid(n=3).map(lambda z: z.ravel().astype(np.float64)) 

1141 pixel_legacy_points = arrays_to_legacy_points(y=pixel_xy.y, x=pixel_xy.x) 

1142 fp_legacy_points = legacy_detector.transform(pixel_legacy_points, PIXELS, FOCAL_PLANE) 

1143 check_transform( 

1144 tc, 

1145 detector.to_focal_plane, 

1146 pixel_xy, 

1147 legacy_points_to_xy_array(fp_legacy_points), 

1148 detector.frame, 

1149 detector.to_focal_plane.out_frame, 

1150 in_atol=1e-9 * u.pix, 

1151 out_atol=1e-7 * detector.to_focal_plane.out_frame.unit, 

1152 ) 

1153 fa_legacy_points = legacy_detector.transform(pixel_legacy_points, PIXELS, FIELD_ANGLE) 

1154 check_transform( 

1155 tc, 

1156 detector.to_field_angle, 

1157 pixel_xy, 

1158 legacy_points_to_xy_array(fa_legacy_points), 

1159 detector.frame, 

1160 detector.to_field_angle.out_frame, 

1161 in_atol=1e-9 * u.pix, 

1162 out_atol=1e-7 * u.arcsec, 

1163 )