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

195 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-18 09:00 +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_images_equal", 

18 "assert_masked_images_equal", 

19 "assert_masks_equal", 

20 "assert_projections_equal", 

21 "check_astropy_wcs_interface", 

22 "check_projection", 

23 "check_transform", 

24 "compare_image_to_legacy", 

25 "compare_mask_to_legacy", 

26 "compare_masked_image_to_legacy", 

27 "compare_projection_to_legacy_wcs", 

28 "compare_psf_to_legacy", 

29 "compare_visit_image_to_legacy", 

30 "legacy_coords_to_astropy", 

31 "legacy_points_to_xy_array", 

32) 

33 

34import unittest 

35from collections.abc import Mapping 

36from typing import Any, Literal, cast 

37 

38import astropy.units as u 

39import astropy.wcs.wcsapi 

40import numpy as np 

41from astropy.coordinates import SkyCoord 

42 

43from .._geom import XY, Box 

44from .._image import Image 

45from .._mask import Mask, MaskPlane 

46from .._masked_image import MaskedImage 

47from .._transforms import DetectorFrame, Frame, Projection, SkyFrame, Transform 

48from .._visit_image import VisitImage 

49from ..psfs import PointSpreadFunction 

50 

51 

52def assert_close( 

53 tc: unittest.TestCase, 

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

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

56 **kwargs: Any, 

57) -> None: 

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

59 

60 Parameters 

61 ---------- 

62 tc 

63 Test case object with assert methods to use. 

64 a 

65 Array, float, or quantity to compare. 

66 b 

67 Array, float, or quantity to compare. 

68 **kwargs 

69 Forwarded to `astropy.units.allclose`. 

70 """ 

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

72 

73 

74def assert_images_equal( 

75 tc: unittest.TestCase, 

76 a: Image, 

77 b: Image, 

78 *, 

79 rtol: float = 0.0, 

80 atol: float = 0.0, 

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

82) -> None: 

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

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

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

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

87 if expect_view is not None: 

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

89 if expect_view == "array": 

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

91 else: 

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

93 if not expect_view: 

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

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

96 

97 

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

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

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

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

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

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

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

105 

106 

107def assert_masked_images_equal( 

108 tc: unittest.TestCase, 

109 a: MaskedImage, 

110 b: MaskedImage, 

111 *, 

112 rtol: float = 0.0, 

113 atol: float = 0.0, 

114 expect_view: bool | None = None, 

115) -> None: 

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

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

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

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

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

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

122 

123 

124def compare_image_to_legacy( 

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

126) -> None: 

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

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

129 if expect_view is not None: 

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

131 if not expect_view: 

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

133 

134 

135def compare_mask_to_legacy( 

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

137) -> None: 

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

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

140 if plane_map is None: 

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

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

143 np.testing.assert_array_equal( 

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

145 mask.get(new_plane.name), 

146 ) 

147 

148 

149def compare_masked_image_to_legacy( 

150 tc: unittest.TestCase, 

151 masked_image: MaskedImage, 

152 legacy_masked_image: Any, 

153 *, 

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

155 expect_view: bool | None = None, 

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

157) -> None: 

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

159 object. 

160 

161 Parameters 

162 ---------- 

163 tc 

164 Test case to use for asserts. 

165 masked_image 

166 New image to test. 

167 legacy_masked_image 

168 Legacy image to test against. 

169 plane_map 

170 Mapping between new and legacy mask planes. 

171 expect_view 

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

173 memory. 

174 alternates 

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

176 check against the legacy versions of those components. 

177 """ 

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

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

180 compare_image_to_legacy( 

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

182 ) 

183 if alternates: 

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

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

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

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

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

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

190 

191 

192def compare_visit_image_to_legacy( 

193 tc: unittest.TestCase, 

194 visit_image: VisitImage, 

195 legacy_exposure: Any, 

196 *, 

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

198 expect_view: bool | None = None, 

199 instrument: str, 

200 visit: int, 

201 detector: int, 

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

203) -> None: 

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

205 object. 

206 

207 Parameters 

208 ---------- 

209 tc 

210 Test case to use for asserts. 

211 visit_image 

212 New image to test. 

213 legacy_exposure 

214 Legacy image to test against. 

215 plane_map 

216 Mapping between new and legacy mask planes. 

217 expect_view 

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

219 memory. 

220 instrument 

221 Expected instrument name. 

222 visit 

223 Expected visit ID. 

224 detetector 

225 Expected detector ID. 

226 alternates 

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

228 check against the legacy versions of those components. 

229 """ 

230 compare_masked_image_to_legacy( 

231 tc, 

232 visit_image, 

233 legacy_exposure.getMaskedImage(), 

234 plane_map=plane_map, 

235 expect_view=expect_view, 

236 alternates=alternates, 

237 ) 

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

239 compare_projection_to_legacy_wcs( 

240 tc, 

241 visit_image.projection, 

242 legacy_exposure.getWcs(), 

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

244 visit_image.bbox, 

245 ) 

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

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

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

249 if alternates: 

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

251 compare_projection_to_legacy_wcs( 

252 tc, 

253 projection, 

254 legacy_exposure.getWcs(), 

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

256 visit_image.bbox, 

257 ) 

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

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

260 

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

262 visitInfo = legacy_exposure.visitInfo 

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

264 

265 

266def compare_psf_to_legacy( 

267 tc: unittest.TestCase, psf: PointSpreadFunction, legacy_psf: Any, subimage_bbox: Box | None = None 

268) -> int: 

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

270 

271 Parameters 

272 ---------- 

273 tc 

274 Test case object with assert methods to use. 

275 psf 

276 Point-spread function to test. 

277 legacy_psf 

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

279 subimage_bbox 

280 Bounding box to draw test points from. Defaults to ``psf.bounds``. 

281 

282 Returns 

283 ------- 

284 `int` 

285 The number of points actually tested. 

286 """ 

287 if subimage_bbox is None: 

288 if isinstance(psf.bounds, Box): 

289 subimage_bbox = psf.bounds 

290 elif hasattr(psf.bounds, "bbox"): 

291 subimage_bbox = cast(Box, psf.bounds.bbox) 

292 else: 

293 raise TypeError("No usable PSF bounds found; 'subimage_bbox' must be provided.") 

294 

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

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

297 legacy_points = arrays_to_legacy_points(pixel_xy.x, pixel_xy.y) 

298 

299 n_points_tested: int = 0 

300 for p in legacy_points: 

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

302 continue 

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

304 tc.assertEqual( 

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

306 ) 

307 tc.assertEqual( 

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

309 ) 

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

311 n_points_tested += 1 

312 return n_points_tested 

313 

314 

315def compare_projection_to_legacy_wcs[F: Frame]( 

316 tc: unittest.TestCase, 

317 projection: Projection[F], 

318 legacy_wcs: Any, 

319 pixel_frame: F, 

320 subimage_bbox: Box, 

321 is_fits: bool = False, 

322) -> None: 

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

324 `lsst.afw.geom.SkyWcs`. 

325 

326 Parameters 

327 ---------- 

328 tc 

329 Test case object with assert methods to use. 

330 projection 

331 Projection to test. 

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

333 Equivalent legacy WCS. 

334 pixel_frame 

335 Expected pixel frame for the projection. 

336 subimage_bbox 

337 Bounding box of points to generate for tests. 

338 is_fits 

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

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

341 attached instead. 

342 """ 

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

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

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

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

347 sky_coords = legacy_coords_to_astropy( 

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

349 ) 

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

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

352 # change. 

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

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

355 # array indices. 

356 check_astropy_wcs_interface( 

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

358 ) 

359 if is_fits: 

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

361 assert fits_wcs is not None 

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

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

364 # Projection from a FITS WCS, too. 

365 fits_projection = Projection.from_fits_wcs(fits_wcs, pixel_frame) 

366 check_projection( 

367 tc, 

368 fits_projection, 

369 subimage_array_xy, 

370 sky_coords, 

371 pixel_frame, 

372 pixel_atol=1e-5, 

373 ) 

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

375 # AST FrameSet so we can convert them into legacy 

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

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

378 else: 

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

380 # The legacy SkyWcs should instead have a FITS approximation 

381 # attached; run the same tests on that. 

382 assert projection.fits_approximation is not None 

383 compare_projection_to_legacy_wcs( 

384 tc, 

385 projection.fits_approximation, 

386 legacy_wcs.getFitsApproximation(), 

387 pixel_frame, 

388 subimage_bbox, 

389 is_fits=True, 

390 ) 

391 

392 

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

394 tc: unittest.TestCase, 

395 transform: Transform[I, O], 

396 input_xy: XY[np.ndarray], 

397 output_xy: XY[np.ndarray], 

398 in_frame: Frame, 

399 out_frame: Frame, 

400 *, 

401 check_inverted: bool = True, 

402 in_atol: u.Quantity | None = None, 

403 out_atol: u.Quantity | None = None, 

404) -> None: 

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

406 

407 Parameters 

408 ---------- 

409 tc 

410 Test case object with assert methods to use. 

411 transform 

412 Transform to test. 

413 input_xy 

414 Arrays of input points. 

415 outout_xy 

416 Arrays of output points. 

417 in_frame 

418 Expected input frame. 

419 out_frame 

420 Expected output frame. 

421 check_inverted 

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

423 in_atol 

424 Expected absolute precision of input points. 

425 out_atol 

426 Expected absolute precision of output points. 

427 """ 

428 tc.assertEqual(transform.in_frame, in_frame) 

429 tc.assertEqual(transform.out_frame, out_frame) 

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

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

432 # Test array interfaces. 

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

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

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

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

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

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

439 # Test scalar interfaces with numpy scalars. 

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

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

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

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

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

445 # Test quantity array interfaces. 

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

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

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

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

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

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

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

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

454 # Test quantity scalar interfaces. 

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

456 input_q_xy.x, input_q_xy.y, output_q_xy.x, output_q_xy.y 

457 ): 

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

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

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

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

462 if check_inverted: 

463 # Test the inverse transform. 

464 check_transform( 

465 tc, 

466 transform.inverted(), 

467 output_xy, 

468 input_xy, 

469 out_frame, 

470 in_frame, 

471 check_inverted=False, 

472 out_atol=in_atol, 

473 in_atol=out_atol, 

474 ) 

475 

476 

477def check_projection[P: Frame]( 

478 tc: unittest.TestCase, 

479 projection: Projection[P], 

480 pixel_xy: XY[np.ndarray], 

481 sky_coords: SkyCoord, 

482 pixel_frame: Frame, 

483 *, 

484 pixel_atol: float | None = None, 

485 sky_atol: u.Quantity | None = None, 

486) -> None: 

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

488 coordinates. 

489 

490 Parameters 

491 ---------- 

492 tc 

493 Test case object with assert methods to use. 

494 projection 

495 Projection to test. 

496 pixel_xy 

497 Arrays of pixel coordinates. 

498 sky_coords 

499 Corresponding sky coordinates. 

500 pixel_frame 

501 Expected pixel frame. 

502 pixel_atol 

503 Expected absolute precision of pixel points. 

504 sky_atol 

505 Expected absolute precision of sky coordinates. 

506 """ 

507 tc.assertEqual(projection.pixel_frame, pixel_frame) 

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

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

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

511 # Test array interfaces. 

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

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

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

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

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

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

518 # Test scalar interfaces. 

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

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

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

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

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

524 # Test the underlying Transform object. 

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

526 check_transform( 

527 tc, 

528 projection.pixel_to_sky_transform, 

529 pixel_xy, 

530 sky_xy, 

531 pixel_frame, 

532 SkyFrame.ICRS, 

533 check_inverted=False, 

534 in_atol=pixel_atol_q, 

535 out_atol=sky_atol, 

536 ) 

537 check_transform( 

538 tc, 

539 projection.sky_to_pixel_transform, 

540 sky_xy, 

541 pixel_xy, 

542 SkyFrame.ICRS, 

543 pixel_frame, 

544 check_inverted=False, 

545 in_atol=sky_atol, 

546 out_atol=pixel_atol_q, 

547 ) 

548 # Test the Astropy interface adapter. 

549 check_astropy_wcs_interface( 

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

551 ) 

552 

553 

554def assert_projections_equal( 

555 tc: unittest.TestCase, 

556 a: Projection[Any] | None, 

557 b: Projection[Any] | None, 

558 expect_identity: bool | None = None, 

559) -> None: 

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

561 if a is None and b is None: 

562 return 

563 assert a is not None and b is not None 

564 match expect_identity: 

565 case True: 

566 tc.assertIs(a, b) 

567 return 

568 case False: 

569 tc.assertIsNot(a, b) 

570 case None if a is b: 

571 return 

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

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

574 assert_projections_equal( 

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

576 ) 

577 

578 

579def check_astropy_wcs_interface( 

580 tc: unittest.TestCase, 

581 wcs: astropy.wcs.wcsapi.BaseHighLevelWCS, 

582 pixel_xy: XY[np.ndarray], 

583 sky_coords: SkyCoord, 

584 *, 

585 pixel_atol: float | None = None, 

586 sky_atol: u.Quantity | None = None, 

587) -> None: 

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

589 sky coordinates. 

590 

591 Parameters 

592 ---------- 

593 tc 

594 Test case object with assert methods to use. 

595 wcs 

596 Astropy WCS object to test. 

597 pixel_xy 

598 Arrays of pixel coordinates. 

599 sky_coords 

600 Corresponding sky coordinates. 

601 pixel_atol 

602 Expected absolute precision of pixel points. 

603 sky_atol 

604 Expected absolute precision of sky coordinates. 

605 """ 

606 test_x, test_y = wcs.world_to_pixel(sky_coords) 

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

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

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

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

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

612 

613 

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

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

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

617 

618 

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

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

621 coordinate object. 

622 """ 

623 return SkyCoord( 

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

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

626 ) 

627 

628 

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

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

631 from lsst.geom import Point2D 

632 

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