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

379 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-20 08:29 +0000

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 projection := alternates.get("projection"): 

347 compare_projection_to_legacy_wcs( 

348 tc, 

349 projection, 

350 legacy_exposure.getWcs(), 

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

352 visit_image.bbox, 

353 ) 

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

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

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

357 compare_observation_summary_stats_to_legacy( 

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

359 ) 

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

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

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

363 visitInfo = legacy_exposure.visitInfo 

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

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

366 compare_aperture_corrections_to_legacy( 

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

368 ) 

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

370 compare_photo_calib_to_legacy( 

371 tc, 

372 photometric_scaling, 

373 legacy_exposure.info.getPhotoCalib(), 

374 applied_legacy_photo_calib=applied_legacy_photo_calib, 

375 subimage_bbox=tiny_bbox, 

376 ) 

377 

378 

379def compare_photo_calib_to_legacy( 

380 tc: unittest.TestCase, 

381 photometric_scaling: BaseField | None, 

382 legacy_photo_calib: LegacyPhotoCalib, 

383 *, 

384 applied_legacy_photo_calib: LegacyPhotoCalib | None = None, 

385 subimage_bbox: Box, 

386) -> None: 

387 if legacy_photo_calib._isConstant: 

388 if legacy_photo_calib.getCalibrationMean() == 1.0: 

389 if applied_legacy_photo_calib is None: 

390 tc.assertIsNone(photometric_scaling) 

391 return 

392 else: 

393 legacy_photo_calib = applied_legacy_photo_calib 

394 if legacy_photo_calib._isConstant: 

395 assert isinstance(photometric_scaling, ChebyshevField) 

396 assert_close( 

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

398 ) 

399 else: 

400 assert photometric_scaling is not None 

401 compare_field_to_legacy( 

402 tc, 

403 photometric_scaling / legacy_photo_calib.getCalibrationMean(), 

404 legacy_photo_calib.computeScaledCalibration(), 

405 subimage_bbox, 

406 ) 

407 

408 

409def compare_cell_coadd_to_legacy( 

410 tc: unittest.TestCase, 

411 cell_coadd: CellCoadd, 

412 legacy_cell_coadd: MultipleCellCoadd, 

413 *, 

414 tract_bbox: Box, 

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

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

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

418) -> None: 

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

420 `lsst.cell_coadds.MultipleCellCoadd` object. 

421 

422 Parameters 

423 ---------- 

424 tc 

425 Test case to use for asserts. 

426 cell_coadd 

427 New coadd to test. 

428 legacy_cell_coadd 

429 Legacy coadd to test against. 

430 tract_bbox 

431 Bounding box of the full tract. 

432 psf_points 

433 Points to use to compare the PSFs. 

434 plane_map 

435 Mapping between new and legacy mask planes. 

436 alternates 

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

438 check against the legacy versions of those components. 

439 """ 

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

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

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

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

444 if legacy_stitched.mask_fractions is not None: 

445 compare_image_to_legacy( 

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

447 ) 

448 for n in range(legacy_stitched.n_noise_realizations): 

449 compare_image_to_legacy( 

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

451 ) 

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

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

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

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

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

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

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

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

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

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

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

463 compare_projection_to_legacy_wcs( 

464 tc, 

465 cell_coadd.projection, 

466 legacy_cell_coadd.wcs, 

467 TractFrame( 

468 skymap=legacy_cell_coadd.identifiers.skymap, 

469 tract=legacy_cell_coadd.identifiers.tract, 

470 bbox=tract_bbox, 

471 ), 

472 cell_coadd.bbox, 

473 is_fits=True, 

474 ) 

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

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

477 compare_psf_to_legacy( 

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

479 ) 

480 compare_cell_coadd_provenance_to_legacy(tc, cell_coadd.provenance, legacy_cell_coadd) 

481 if alternates: 

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

483 compare_projection_to_legacy_wcs( 

484 tc, 

485 projection, 

486 legacy_stitched.wcs, 

487 TractFrame( 

488 skymap=legacy_cell_coadd.identifiers.skymap, 

489 tract=legacy_cell_coadd.identifiers.tract, 

490 bbox=tract_bbox, 

491 ), 

492 cell_coadd.bbox, 

493 is_fits=True, 

494 ) 

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

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

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

498 compare_cell_coadd_provenance_to_legacy(tc, provenance, legacy_cell_coadd) 

499 

500 

501def compare_cell_coadd_provenance_to_legacy( 

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

503) -> None: 

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

505 `lsst.cell_coadds.MultipleCellCoadd` object. 

506 

507 Parameters 

508 ---------- 

509 tc 

510 Test case to use for asserts. 

511 provenance 

512 New provenance object to test. 

513 legacy_cell_coadd 

514 Legacy coadd to test against. 

515 """ 

516 from lsst.cell_coadds import ObservationIdentifiers 

517 

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

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

520 prov = provenance[cell_index] 

521 legacy_table = astropy.table.Table( 

522 rows=[ 

523 [ 

524 ids.instrument, 

525 ids.visit, 

526 ids.detector, 

527 ids.day_obs, 

528 ids.physical_filter, 

529 legacy_input.overlaps_center, 

530 legacy_input.overlap_fraction, 

531 legacy_input.weight, 

532 legacy_input.psf_shape.getIxx(), 

533 legacy_input.psf_shape.getIyy(), 

534 legacy_input.psf_shape.getIxy(), 

535 legacy_input.psf_shape_flag, 

536 ] 

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

538 ], 

539 dtype=[ 

540 np.object_, 

541 np.uint64, 

542 np.uint16, 

543 np.uint32, 

544 np.object_, 

545 np.bool_, 

546 np.float64, 

547 np.float64, 

548 np.float64, 

549 np.float64, 

550 np.float64, 

551 np.bool_, 

552 ], 

553 names=[ 

554 "instrument", 

555 "visit", 

556 "detector", 

557 "day_obs", 

558 "physical_filter", 

559 "overlaps_center", 

560 "overlap_fraction", 

561 "weight", 

562 "psf_shape_xx", 

563 "psf_shape_yy", 

564 "psf_shape_xy", 

565 "psf_shape_flag", 

566 ], 

567 ) 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

583 np.testing.assert_array_equal( 

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

585 ) 

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

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

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

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

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

591 for row in prov.inputs: 

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

593 legacy_polygon = legacy_cell_coadd.common.visit_polygons[polygon_key] 

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

595 

596 

597def compare_psf_to_legacy( 

598 tc: unittest.TestCase, 

599 psf: PointSpreadFunction, 

600 legacy_psf: Any, 

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

602 expect_legacy_raise_on_out_of_bounds: bool = False, 

603) -> int: 

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

605 

606 Parameters 

607 ---------- 

608 tc 

609 Test case object with assert methods to use. 

610 psf 

611 Point-spread function to test. 

612 legacy_psf 

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

614 points 

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

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

617 expect_legacy_raise_on_out_of_bounds 

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

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

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

621 

622 Returns 

623 ------- 

624 `int` 

625 The number of points actually tested. 

626 """ 

627 from lsst.afw.detection import InvalidPsfError 

628 

629 if points is None: 

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

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

632 n_points_tested: int = 0 

633 for p in legacy_points: 

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

635 if expect_legacy_raise_on_out_of_bounds: 

636 with tc.assertRaises(InvalidPsfError): 

637 legacy_psf.computeKernelImage(p) 

638 continue 

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

640 tc.assertEqual( 

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

642 ) 

643 tc.assertEqual( 

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

645 ) 

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

647 n_points_tested += 1 

648 return n_points_tested 

649 

650 

651def compare_field_to_legacy( 

652 tc: unittest.TestCase, 

653 field: BaseField, 

654 legacy_field: Any, 

655 subimage_bbox: Box, 

656) -> None: 

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

658 `lsst.afw.math.BoundedField`. 

659 

660 Parameters 

661 ---------- 

662 tc 

663 Test case object with assert methods to use. 

664 field 

665 Field to test. 

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

667 Equivalent legacy bounded field. 

668 subimage_bbox 

669 Bounding box for full-image tests. 

670 """ 

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

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

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

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

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

676 legacy_field.addToImage(legacy_image_1, overlapOnly=True) 

677 assert_images_equal( 

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

679 ) 

680 

681 

682def compare_aperture_corrections_to_legacy( 

683 tc: unittest.TestCase, 

684 aperture_corrections: ApertureCorrectionMap, 

685 legacy_ap_corr_map: Any, 

686 subimage_bbox: Box, 

687) -> None: 

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

689 `lsst.afw.image.ApCorrMap`. 

690 

691 Parameters 

692 ---------- 

693 tc 

694 Test case object with assert methods to use. 

695 aperture_corrections 

696 Dictionary to test. 

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

698 Equivalent legacy aperture correction map. 

699 subimage_bbox 

700 Bounding box for full-image tests. 

701 """ 

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

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

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

705 

706 

707def compare_observation_summary_stats_to_legacy( 

708 tc: unittest.TestCase, 

709 summary_stats: ObservationSummaryStats, 

710 legacy_summary_stats: Any, 

711) -> None: 

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

713 `lsst.afw.image.ExposureSummaryStats`. 

714 

715 Parameters 

716 ---------- 

717 tc 

718 Test case object with assert methods to use. 

719 summary_stats 

720 Struct to test. 

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

722 Equivalent legacy struct. 

723 """ 

724 for field in dataclasses.fields(legacy_summary_stats): 

725 a = getattr(legacy_summary_stats, field.name) 

726 b = getattr(summary_stats, field.name) 

727 if isinstance(b, tuple): 

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

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

730 else: 

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

732 

733 

734def compare_projection_to_legacy_wcs[F: Frame]( 

735 tc: unittest.TestCase, 

736 projection: Projection[F], 

737 legacy_wcs: Any, 

738 pixel_frame: F, 

739 subimage_bbox: Box, 

740 is_fits: bool = False, 

741) -> None: 

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

743 `lsst.afw.geom.SkyWcs`. 

744 

745 Parameters 

746 ---------- 

747 tc 

748 Test case object with assert methods to use. 

749 projection 

750 Projection to test. 

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

752 Equivalent legacy WCS. 

753 pixel_frame 

754 Expected pixel frame for the projection. 

755 subimage_bbox 

756 Bounding box of points to generate for tests. 

757 is_fits 

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

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

760 attached instead. 

761 """ 

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

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

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

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

766 sky_coords = legacy_coords_to_astropy( 

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

768 ) 

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

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

771 # change. 

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

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

774 # array indices. 

775 check_astropy_wcs_interface( 

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

777 ) 

778 if is_fits: 

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

780 assert fits_wcs is not None 

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

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

783 # Projection from a FITS WCS, too. 

784 fits_projection = Projection.from_fits_wcs(fits_wcs, pixel_frame) 

785 check_projection( 

786 tc, 

787 fits_projection, 

788 subimage_array_xy, 

789 sky_coords, 

790 pixel_frame, 

791 pixel_atol=1e-5, 

792 ) 

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

794 # AST FrameSet so we can convert them into legacy 

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

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

797 else: 

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

799 # The legacy SkyWcs should instead have a FITS approximation 

800 # attached; run the same tests on that. 

801 assert projection.fits_approximation is not None 

802 compare_projection_to_legacy_wcs( 

803 tc, 

804 projection.fits_approximation, 

805 legacy_wcs.getFitsApproximation(), 

806 pixel_frame, 

807 subimage_bbox, 

808 is_fits=True, 

809 ) 

810 

811 

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

813 tc: unittest.TestCase, 

814 transform: Transform[I, O], 

815 input_xy: XY[np.ndarray], 

816 output_xy: XY[np.ndarray], 

817 in_frame: Frame, 

818 out_frame: Frame, 

819 *, 

820 check_inverted: bool = True, 

821 in_atol: u.Quantity | None = None, 

822 out_atol: u.Quantity | None = None, 

823) -> None: 

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

825 

826 Parameters 

827 ---------- 

828 tc 

829 Test case object with assert methods to use. 

830 transform 

831 Transform to test. 

832 input_xy 

833 Arrays of input points. 

834 output_xy 

835 Arrays of output points. 

836 in_frame 

837 Expected input frame. 

838 out_frame 

839 Expected output frame. 

840 check_inverted 

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

842 in_atol 

843 Expected absolute precision of input points. 

844 out_atol 

845 Expected absolute precision of output points. 

846 """ 

847 tc.assertEqual(transform.in_frame, in_frame) 

848 tc.assertEqual(transform.out_frame, out_frame) 

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

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

851 # Test array interfaces. 

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

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

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

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

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

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

858 # Test scalar interfaces with numpy scalars. 

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

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

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

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

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

864 # Test quantity array interfaces. 

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

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

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

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

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

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

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

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

873 # Test quantity scalar interfaces. 

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

875 input_q_xy.x, input_q_xy.y, output_q_xy.x, output_q_xy.y 

876 ): 

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

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

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

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

881 if check_inverted: 

882 # Test the inverse transform. 

883 check_transform( 

884 tc, 

885 transform.inverted(), 

886 output_xy, 

887 input_xy, 

888 out_frame, 

889 in_frame, 

890 check_inverted=False, 

891 out_atol=in_atol, 

892 in_atol=out_atol, 

893 ) 

894 

895 

896def check_projection[P: Frame]( 

897 tc: unittest.TestCase, 

898 projection: Projection[P], 

899 pixel_xy: XY[np.ndarray], 

900 sky_coords: SkyCoord, 

901 pixel_frame: Frame, 

902 *, 

903 pixel_atol: float | None = None, 

904 sky_atol: u.Quantity | None = None, 

905) -> None: 

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

907 coordinates. 

908 

909 Parameters 

910 ---------- 

911 tc 

912 Test case object with assert methods to use. 

913 projection 

914 Projection to test. 

915 pixel_xy 

916 Arrays of pixel coordinates. 

917 sky_coords 

918 Corresponding sky coordinates. 

919 pixel_frame 

920 Expected pixel frame. 

921 pixel_atol 

922 Expected absolute precision of pixel points. 

923 sky_atol 

924 Expected absolute precision of sky coordinates. 

925 """ 

926 tc.assertEqual(projection.pixel_frame, pixel_frame) 

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

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

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

930 # Test array interfaces. 

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

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

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

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

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

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

937 # Test scalar interfaces. 

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

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

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

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

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

943 # Test the underlying Transform object. 

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

945 check_transform( 

946 tc, 

947 projection.pixel_to_sky_transform, 

948 pixel_xy, 

949 sky_xy, 

950 pixel_frame, 

951 SkyFrame.ICRS, 

952 check_inverted=False, 

953 in_atol=pixel_atol_q, 

954 out_atol=sky_atol, 

955 ) 

956 check_transform( 

957 tc, 

958 projection.sky_to_pixel_transform, 

959 sky_xy, 

960 pixel_xy, 

961 SkyFrame.ICRS, 

962 pixel_frame, 

963 check_inverted=False, 

964 in_atol=sky_atol, 

965 out_atol=pixel_atol_q, 

966 ) 

967 # Test the Astropy interface adapter. 

968 check_astropy_wcs_interface( 

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

970 ) 

971 

972 

973def assert_projections_equal( 

974 tc: unittest.TestCase, 

975 a: Projection[Any] | None, 

976 b: Projection[Any] | None, 

977 expect_identity: bool | None = None, 

978) -> None: 

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

980 if a is None and b is None: 

981 return 

982 assert a is not None and b is not None 

983 match expect_identity: 

984 case True: 

985 tc.assertIs(a, b) 

986 return 

987 case False: 

988 tc.assertIsNot(a, b) 

989 case None if a is b: 

990 return 

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

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

993 assert_projections_equal( 

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

995 ) 

996 

997 

998def check_astropy_wcs_interface( 

999 tc: unittest.TestCase, 

1000 wcs: astropy.wcs.wcsapi.BaseHighLevelWCS, 

1001 pixel_xy: XY[np.ndarray], 

1002 sky_coords: SkyCoord, 

1003 *, 

1004 pixel_atol: float | None = None, 

1005 sky_atol: u.Quantity | None = None, 

1006) -> None: 

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

1008 sky coordinates. 

1009 

1010 Parameters 

1011 ---------- 

1012 tc 

1013 Test case object with assert methods to use. 

1014 wcs 

1015 Astropy WCS object to test. 

1016 pixel_xy 

1017 Arrays of pixel coordinates. 

1018 sky_coords 

1019 Corresponding sky coordinates. 

1020 pixel_atol 

1021 Expected absolute precision of pixel points. 

1022 sky_atol 

1023 Expected absolute precision of sky coordinates. 

1024 """ 

1025 test_x, test_y = wcs.world_to_pixel(sky_coords) 

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

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

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

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

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

1031 

1032 

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

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

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

1036 

1037 

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

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

1040 coordinate object. 

1041 """ 

1042 return SkyCoord( 

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

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

1045 ) 

1046 

1047 

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

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

1050 from lsst.geom import Point2D 

1051 

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

1053 

1054 

1055def compare_amplifier_to_legacy( 

1056 tc: unittest.TestCase, 

1057 amplifier: Amplifier, 

1058 legacy_amplifier: Any, 

1059 *, 

1060 is_raw_assembled: bool, 

1061 expect_nominal_calibrations: bool = True, 

1062) -> None: 

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

1064 `lsst.afw.cameraGeom.Amplifier`. 

1065 """ 

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

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

1068 if is_raw_assembled: 

1069 raw_geom = amplifier.assembled_raw_geometry 

1070 else: 

1071 raw_geom = amplifier.unassembled_raw_geometry 

1072 assert raw_geom is not None 

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

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

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

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

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

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

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

1080 tc.assertEqual( 

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

1082 ) 

1083 tc.assertEqual( 

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

1085 ) 

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

1087 if expect_nominal_calibrations: 

1088 assert amplifier.nominal_calibrations is not None 

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

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

1091 assert_equal_allow_nan( 

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

1093 ) 

1094 assert_equal_allow_nan( 

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

1096 ) 

1097 np.testing.assert_array_equal( 

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

1099 ) 

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

1101 

1102 

1103def compare_detector_to_legacy( 

1104 tc: unittest.TestCase, 

1105 detector: Detector, 

1106 legacy_detector: Any, 

1107 *, 

1108 is_raw_assembled: bool, 

1109 expect_nominal_calibrations: bool = True, 

1110) -> None: 

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

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

1113 

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

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

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

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

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

1119 legacy_orientation = legacy_detector.getOrientation() 

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

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

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

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

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

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

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

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

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

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

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

1131 compare_amplifier_to_legacy( 

1132 tc, 

1133 amplifier, 

1134 legacy_amplifier, 

1135 is_raw_assembled=is_raw_assembled, 

1136 expect_nominal_calibrations=expect_nominal_calibrations, 

1137 ) 

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

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

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

1141 check_transform( 

1142 tc, 

1143 detector.to_focal_plane, 

1144 pixel_xy, 

1145 legacy_points_to_xy_array(fp_legacy_points), 

1146 detector.frame, 

1147 detector.to_focal_plane.out_frame, 

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

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

1150 ) 

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

1152 check_transform( 

1153 tc, 

1154 detector.to_field_angle, 

1155 pixel_xy, 

1156 legacy_points_to_xy_array(fa_legacy_points), 

1157 detector.frame, 

1158 detector.to_field_angle.out_frame, 

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

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

1161 )