Coverage for tests / test_visit_image.py: 15%

268 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-13 01:50 -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 

14import os 

15import unittest 

16import warnings 

17from typing import Any 

18 

19import astropy.io.fits 

20import astropy.units as u 

21import astropy.wcs 

22import numpy as np 

23from astro_metadata_translator import ObservationInfo 

24 

25from lsst.images import ( 

26 Box, 

27 DetectorFrame, 

28 Image, 

29 MaskPlane, 

30 MaskSchema, 

31 ObservationSummaryStats, 

32 ProjectionAstropyView, 

33 TractFrame, 

34 VisitImage, 

35 get_legacy_visit_image_mask_planes, 

36) 

37from lsst.images.aperture_corrections import ApertureCorrectionMap, aperture_corrections_to_legacy 

38from lsst.images.cameras import Detector 

39from lsst.images.fields import ChebyshevField 

40from lsst.images.fits import ExtensionKey, FitsOpaqueMetadata 

41from lsst.images.json import read as read_json 

42from lsst.images.psfs import GaussianPointSpreadFunction, PointSpreadFunction 

43from lsst.images.tests import ( 

44 DP2_VISIT_DETECTOR_DATA_ID, 

45 RoundtripFits, 

46 TemporaryButler, 

47 assert_masked_images_equal, 

48 assert_projections_equal, 

49 compare_aperture_corrections_to_legacy, 

50 compare_detector_to_legacy, 

51 compare_visit_image_to_legacy, 

52 make_random_projection, 

53) 

54 

55EXTERNAL_DATA_DIR = os.environ.get("TESTDATA_IMAGES_DIR", None) 

56LOCAL_DATA_DIR = os.path.join(os.path.dirname(__file__), "data") 

57 

58 

59class VisitImageTestCase(unittest.TestCase): 

60 """Basic Tests for VisitImage.""" 

61 

62 @classmethod 

63 def setUpClass(cls) -> None: 

64 cls.rng = np.random.default_rng(500) 

65 det_frame = DetectorFrame(instrument="Inst", visit=1234, detector=1, bbox=Box.factory[1:4096, 1:4096]) 

66 cls.projection = make_random_projection(cls.rng, det_frame, Box.factory[1:4096, 1:4096]) 

67 cls.mask_schema = MaskSchema([MaskPlane("M1", "D1")]) 

68 cls.obs_info = ObservationInfo(instrument="LSSTCam", detector_num=4) 

69 cls.summary_stats = ObservationSummaryStats(psfSigma=2.5, zeroPoint=31.4) 

70 cls.gaussian_psf = GaussianPointSpreadFunction(2.5, stamp_size=33, bounds=Box.factory[-10:10, -12:13]) 

71 cls.aperture_corrections: ApertureCorrectionMap = { 

72 "flux1": ChebyshevField(det_frame.bbox, np.array([0.75])), 

73 "flux2": ChebyshevField(det_frame.bbox, np.array([0.625])), 

74 } 

75 cls.detector, _, _ = read_json(Detector, os.path.join(LOCAL_DATA_DIR, "detector.json")) 

76 

77 opaque = FitsOpaqueMetadata() 

78 hdr = astropy.io.fits.Header() 

79 with warnings.catch_warnings(): 

80 # Silence warnings about long keys becoming HIERARCH. 

81 warnings.simplefilter("ignore", category=astropy.io.fits.verify.VerifyWarning) 

82 hdr.update({"PLATFORM": "lsstcam", "LSST BUTLER ID": "123456789"}) 

83 opaque.extract_legacy_primary_header(hdr) 

84 

85 cls.image = Image(42, shape=(1024, 1024), unit=u.nJy) 

86 cls.variance = Image(5.0, shape=(1024, 1024), unit=u.nJy * u.nJy) 

87 # API signature suggests projection and obs_info can be None but they 

88 # are required (unless you pass them in via the image plane). 

89 cls.visit_image = VisitImage( 

90 cls.image, 

91 variance=cls.variance, 

92 psf=GaussianPointSpreadFunction(2.5, stamp_size=33, bounds=Box.factory[-10:10, -12:13]), 

93 mask_schema=cls.mask_schema, 

94 projection=cls.projection, 

95 obs_info=cls.obs_info, 

96 summary_stats=cls.summary_stats, 

97 detector=cls.detector, 

98 aperture_corrections=cls.aperture_corrections, 

99 ) 

100 cls.visit_image._opaque_metadata = opaque 

101 cls.simplest_visit_image = VisitImage( 

102 cls.image, 

103 psf=GaussianPointSpreadFunction(2.5, stamp_size=33, bounds=Box.factory[-10:10, -12:13]), 

104 mask_schema=cls.mask_schema, 

105 projection=cls.projection, 

106 detector=cls.detector, 

107 obs_info=cls.obs_info, 

108 ) 

109 

110 def test_basics(self) -> None: 

111 """Test basic constructor patterns.""" 

112 # Test default fill of variance. 

113 visit = self.simplest_visit_image 

114 self.assertEqual(visit.variance.array[0, 0], 1.0) 

115 self.assertIs(visit[...], visit) 

116 self.assertEqual(str(visit), "VisitImage(Image([y=0:1024, x=0:1024], int64), ['M1'])") 

117 self.assertEqual( 

118 repr(visit), 

119 "VisitImage(Image(..., bbox=Box(y=Interval(start=0, stop=1024), x=Interval(start=0, stop=1024))," 

120 " dtype=dtype('int64')), mask_schema=MaskSchema([MaskPlane(name='M1', description='D1')]," 

121 " dtype=dtype('uint8')))", 

122 ) 

123 

124 astropy_wcs = visit.astropy_wcs 

125 self.assertIsInstance(astropy_wcs, ProjectionAstropyView) 

126 approx_wcs = visit.fits_wcs 

127 self.assertIsInstance(approx_wcs, astropy.wcs.WCS) 

128 

129 with self.assertRaises(TypeError): 

130 # Requires a PSF. 

131 VisitImage( 

132 self.image, 

133 mask_schema=self.mask_schema, 

134 projection=self.projection, 

135 obs_info=self.obs_info, 

136 detector=self.detector, 

137 ) 

138 

139 with self.assertRaises(TypeError): 

140 # Requires ObservationInfo. 

141 VisitImage( 

142 self.image, 

143 psf=self.gaussian_psf, 

144 mask_schema=self.mask_schema, 

145 projection=self.projection, 

146 detector=self.detector, 

147 ) 

148 

149 with self.assertRaises(TypeError): 

150 # Requires a projection. 

151 VisitImage( 

152 self.image, 

153 psf=self.gaussian_psf, 

154 mask_schema=self.mask_schema, 

155 obs_info=self.obs_info, 

156 detector=self.detector, 

157 ) 

158 

159 with self.assertRaises(TypeError): 

160 # Requires a detector. 

161 VisitImage( 

162 self.image, 

163 psf=self.gaussian_psf, 

164 mask_schema=self.mask_schema, 

165 projection=self.projection, 

166 obs_info=self.obs_info, 

167 ) 

168 

169 with self.assertRaises(TypeError): 

170 # Requires some form of mask. 

171 VisitImage( 

172 self.image, 

173 psf=self.gaussian_psf, 

174 projection=self.projection, 

175 obs_info=self.obs_info, 

176 detector=self.detector, 

177 ) 

178 

179 with self.assertRaises(TypeError): 

180 VisitImage( 

181 Image(42, shape=(5, 5)), 

182 psf=self.gaussian_psf, 

183 mask_schema=self.mask_schema, 

184 projection=self.projection, 

185 obs_info=self.obs_info, 

186 detector=self.detector, 

187 ) 

188 

189 # Requires a DetectorFrame. 

190 tract_frame = TractFrame(skymap="Skymap", tract=1, bbox=Box.factory[1:10, 1:10]) 

191 tract_proj = make_random_projection(self.rng, tract_frame, Box.factory[1:4096, 1:4096]) 

192 with self.assertRaises(TypeError): 

193 VisitImage( 

194 self.image, 

195 projection=tract_proj, 

196 psf=self.gaussian_psf, 

197 mask_schema=self.mask_schema, 

198 obs_info=self.obs_info, 

199 detector=self.detector, 

200 ) 

201 

202 # Variance unit mismatch. 

203 with self.assertRaises(ValueError): 

204 VisitImage( 

205 self.image, 

206 variance=self.image, 

207 psf=self.gaussian_psf, 

208 mask_schema=self.mask_schema, 

209 projection=self.projection, 

210 obs_info=self.obs_info, 

211 detector=self.detector, 

212 ) 

213 

214 def test_copy_and_slice(self) -> None: 

215 """Test that arrays and components are copied (when not immutable) by 

216 'copy' and referenced by 'slice'. 

217 """ 

218 visit = self.visit_image 

219 copy = visit.copy() 

220 copy.image.array[0, 0] = 30.0 

221 self.assertEqual(visit.image.array[0, 0], 42.0) 

222 self.assertEqual(copy.image.array[0, 0], 30.0) 

223 subvisit = visit[Box.factory[0:5, 0:5]] 

224 # Check summary stats. 

225 self.assertEqual(copy.summary_stats, visit.summary_stats) 

226 self.assertIsNot(copy.summary_stats, visit.summary_stats) 

227 self.assertEqual(subvisit.summary_stats, visit.summary_stats) 

228 self.assertIs(subvisit.summary_stats, visit.summary_stats) 

229 # Check aperture corrections. 

230 self.assertEqual(copy.aperture_corrections.keys(), visit.aperture_corrections.keys()) 

231 self.assertIsNot(copy.aperture_corrections, visit.aperture_corrections) 

232 self.assertEqual(subvisit.aperture_corrections.keys(), visit.aperture_corrections.keys()) 

233 self.assertIs(subvisit.aperture_corrections, visit.aperture_corrections) 

234 

235 def test_obs_info(self) -> None: 

236 """Check that ObservationInfo has been constructed.""" 

237 visit = self.visit_image 

238 self.assertIsNotNone(visit.obs_info) 

239 self.maxDiff = None 

240 assert visit.obs_info is not None # for mypy. 

241 self.assertEqual(visit.obs_info.instrument, "LSSTCam") 

242 

243 def test_summary_stats(self) -> None: 

244 """Test the comparisons and attributes of ObservationSummaryStats.""" 

245 self.assertEqual(self.summary_stats, ObservationSummaryStats(psfSigma=2.5, zeroPoint=31.4)) 

246 self.assertNotEqual(self.summary_stats, ObservationSummaryStats(psfSigma=2.5)) 

247 self.assertNotEqual( 

248 self.summary_stats, ObservationSummaryStats(psfSigma=2.5, raCorners=(5.2, 5.4, 5.4, 5.2)) 

249 ) 

250 

251 def test_read_write(self) -> None: 

252 """Test that a visit can round trip through a FITS file.""" 

253 with RoundtripFits(self, self.visit_image, "VisitImage") as roundtrip: 

254 # Check that we're still using the right compression, and that we 

255 # wrote WCSs. 

256 fits = roundtrip.inspect() 

257 self.assertEqual(fits[1].header["ZCMPTYPE"], "GZIP_2") 

258 self.assertEqual(fits[1].header["CTYPE1"], "RA---TAN") 

259 self.assertEqual(fits[2].header["ZCMPTYPE"], "GZIP_2") 

260 self.assertEqual(fits[2].header["CTYPE1"], "RA---TAN") 

261 self.assertEqual(fits[3].header["ZCMPTYPE"], "GZIP_2") 

262 self.assertEqual(fits[3].header["CTYPE1"], "RA---TAN") 

263 # Check a subimage read. 

264 subbox = Box.factory[8:13, 9:30] 

265 subimage = roundtrip.get(bbox=subbox) 

266 assert_masked_images_equal(self, subimage, self.visit_image[subbox], expect_view=False) 

267 with self.subTest(): 

268 self.assertEqual(roundtrip.get("bbox"), self.visit_image.bbox) 

269 with self.subTest(): 

270 obs_info = roundtrip.get("obs_info") 

271 self.assertIsInstance(obs_info, ObservationInfo) 

272 self.assertEqual(obs_info, self.visit_image.obs_info) 

273 with self.subTest(): 

274 summary_stats = roundtrip.get("summary_stats") 

275 self.assertIsInstance(summary_stats, ObservationSummaryStats) 

276 self.assertEqual(summary_stats, self.visit_image.summary_stats) 

277 with self.subTest(): 

278 psf = roundtrip.get("psf") 

279 self.assertIsInstance(psf, GaussianPointSpreadFunction) 

280 self.assertEqual(psf.kernel_bbox, self.gaussian_psf.kernel_bbox) 

281 

282 assert_masked_images_equal(self, roundtrip.result, self.visit_image, expect_view=False) 

283 # Check that the round-tripped headers are the same (up to card order). 

284 self.assertEqual(len(roundtrip.result._opaque_metadata.headers[ExtensionKey()]), 1) 

285 self.assertEqual( 

286 dict(self.visit_image._opaque_metadata.headers[ExtensionKey()]), 

287 dict(roundtrip.result._opaque_metadata.headers[ExtensionKey()]), 

288 ) 

289 self.assertFalse(roundtrip.result._opaque_metadata.headers[ExtensionKey("IMAGE")]) 

290 self.assertFalse(roundtrip.result._opaque_metadata.headers[ExtensionKey("MASK")]) 

291 self.assertFalse(roundtrip.result._opaque_metadata.headers[ExtensionKey("VARIANCE")]) 

292 self.assertEqual(roundtrip.result.obs_info, self.visit_image.obs_info) 

293 self.assertIsNotNone(roundtrip.result.summary_stats) 

294 self.assertEqual( 

295 roundtrip.result.summary_stats.psfSigma, 

296 self.visit_image.summary_stats.psfSigma, 

297 ) 

298 self.assertEqual( 

299 roundtrip.result.summary_stats.zeroPoint, 

300 self.visit_image.summary_stats.zeroPoint, 

301 ) 

302 

303 

304@unittest.skipUnless(EXTERNAL_DATA_DIR is not None, "TESTDATA_IMAGES_DIR is not in the environment.") 

305class VisitImageLegacyTestCase(unittest.TestCase): 

306 """Tests for the VisitImage class and the basics of the archive system. 

307 

308 Requires legacy code. 

309 """ 

310 

311 @classmethod 

312 def setUpClass(cls) -> None: 

313 assert EXTERNAL_DATA_DIR is not None, "Guaranteed by decorator." 

314 cls.filename = os.path.join(EXTERNAL_DATA_DIR, "dp2", "legacy", "visit_image.fits") 

315 try: 

316 from lsst.afw.image import ExposureFitsReader 

317 

318 cls.legacy_exposure = ExposureFitsReader(cls.filename).read() 

319 except ImportError: 

320 raise unittest.SkipTest("afw not available; cannot read legacy visit images") from None 

321 cls.plane_map = plane_map = get_legacy_visit_image_mask_planes() 

322 cls.visit_image = VisitImage.read_legacy( 

323 cls.filename, preserve_quantization=True, plane_map=plane_map 

324 ) 

325 

326 def test_legacy_errors(self) -> None: 

327 """Legacy read failure modes.""" 

328 with self.assertRaises(ValueError): 

329 VisitImage.from_legacy(self.legacy_exposure, instrument="HSC") 

330 with self.assertRaises(ValueError): 

331 VisitImage.from_legacy(self.legacy_exposure, visit=123456) 

332 with self.assertRaises(ValueError): 

333 VisitImage.from_legacy(self.legacy_exposure, unit=u.mJy) 

334 visit = VisitImage.from_legacy( 

335 self.legacy_exposure, instrument="LSSTCam", unit=u.nJy, visit=2025052000177 

336 ) 

337 self.assertEqual(visit.unit, u.nJy) 

338 

339 with self.assertRaises(ValueError): 

340 VisitImage.read_legacy(self.filename, instrument="HSC") 

341 with self.assertRaises(ValueError): 

342 VisitImage.read_legacy(self.filename, visit=123456) 

343 

344 def test_component_reads(self) -> None: 

345 """Test reads of components from legacy file.""" 

346 visit = VisitImage.read_legacy(self.filename) 

347 proj = VisitImage.read_legacy(self.filename, component="projection") 

348 assert_projections_equal(self, proj, visit.projection, expect_identity=False) 

349 image = VisitImage.read_legacy(self.filename, component="image") 

350 self.assertEqual(image, visit.image) 

351 self.check_legacy_obs_info(image.obs_info) 

352 assert_projections_equal(self, proj, image.projection, expect_identity=False) 

353 variance = VisitImage.read_legacy(self.filename, component="variance") 

354 self.assertEqual(variance, visit.variance) 

355 assert_projections_equal(self, proj, variance.projection, expect_identity=False) 

356 self.check_legacy_obs_info(variance.obs_info) 

357 mask = VisitImage.read_legacy(self.filename, component="mask") 

358 self.assertEqual(mask, visit.mask) 

359 assert_projections_equal(self, proj, mask.projection, expect_identity=False) 

360 self.check_legacy_obs_info(mask.obs_info) 

361 psf = VisitImage.read_legacy(self.filename, component="psf") 

362 self.assertIsInstance(psf, PointSpreadFunction) 

363 obs_info = VisitImage.read_legacy(self.filename, component="obs_info") 

364 self.check_legacy_obs_info(obs_info) 

365 summary_stats = VisitImage.read_legacy(self.filename, component="summary_stats") 

366 self.assertIsInstance(summary_stats, ObservationSummaryStats) 

367 self.assertEqual(summary_stats.nPsfStar, 93) 

368 compare_aperture_corrections_to_legacy( 

369 self, 

370 VisitImage.read_legacy(self.filename, component="aperture_corrections"), 

371 self.legacy_exposure.info.getApCorrMap(), 

372 visit.bbox, 

373 ) 

374 detector = VisitImage.read_legacy(self.filename, component="detector") 

375 compare_detector_to_legacy(self, detector, self.legacy_exposure.getDetector(), is_raw_assembled=True) 

376 

377 def check_legacy_obs_info(self, obs_info: ObservationInfo | None) -> None: 

378 """Check that an `ObservationInfo` instance is not `None`, and that it 

379 matches the one in the legacy test data file. 

380 """ 

381 self.assertIsInstance(obs_info, ObservationInfo) 

382 self.assertEqual(obs_info.instrument, "LSSTCam") 

383 self.assertEqual(obs_info.detector_num, 85, obs_info) 

384 self.assertEqual(obs_info.detector_unique_name, "R21_S11", obs_info) 

385 self.assertEqual(obs_info.physical_filter, "r_57", obs_info) 

386 

387 def test_obs_info(self) -> None: 

388 """Check that ObservationInfo has been constructed.""" 

389 legacy = VisitImage.from_legacy(self.legacy_exposure, plane_map=self.plane_map) 

390 self.assertIsNotNone(legacy.obs_info) 

391 self.maxDiff = None 

392 self.assertEqual(legacy.obs_info, self.visit_image.obs_info) 

393 assert legacy.obs_info is not None # for mypy. 

394 self.assertEqual(legacy.obs_info.instrument, "LSSTCam") 

395 self.assertEqual(legacy.obs_info.detector_num, 85, legacy.obs_info) 

396 self.assertEqual(legacy.obs_info.detector_unique_name, "R21_S11", legacy.obs_info) 

397 self.assertEqual(legacy.obs_info.physical_filter, "r_57", legacy.obs_info) 

398 

399 def test_aperture_corrections_to_legacy(self) -> None: 

400 """Test that we can convert an aperture correction map back to a 

401 legacy `lsst.afw.image.ApCorrMap`. 

402 """ 

403 legacy_ap_corr_map = aperture_corrections_to_legacy(self.visit_image.aperture_corrections) 

404 compare_aperture_corrections_to_legacy( 

405 self, self.visit_image.aperture_corrections, legacy_ap_corr_map, self.visit_image.bbox 

406 ) 

407 

408 def test_read_legacy_headers(self) -> None: 

409 """Test that headers were correctly stripped and interpreted in 

410 `VisitImage.read_legacy`. 

411 """ 

412 # Check that we read the units from BUNIT. 

413 self.assertEqual(self.visit_image.unit, astropy.units.nJy) 

414 # Check that the primary header has the keys we want, and none of the 

415 # keys we don't want. 

416 header = self.visit_image._opaque_metadata.headers[ExtensionKey()] 

417 self.assertIn("EXPTIME", header) 

418 self.assertEqual(header["PLATFORM"], "lsstcam") 

419 self.assertNotIn("LSST BUTLER ID", header) 

420 self.assertNotIn("AR HDU", header) 

421 self.assertNotIn("A_ORDER", header) 

422 # Check that the extension HDUs do not have any custom headers. 

423 self.assertFalse(self.visit_image._opaque_metadata.headers[ExtensionKey("IMAGE")]) 

424 self.assertFalse(self.visit_image._opaque_metadata.headers[ExtensionKey("MASK")]) 

425 self.assertFalse(self.visit_image._opaque_metadata.headers[ExtensionKey("VARIANCE")]) 

426 

427 def test_from_legacy_headers(self) -> None: 

428 """Test that from_legacy handles headers properly.""" 

429 legacy = VisitImage.from_legacy(self.legacy_exposure, plane_map=self.plane_map) 

430 header = legacy._opaque_metadata.headers[ExtensionKey()] 

431 self.assertIn("EXPTIME", header) 

432 self.assertEqual(header["PLATFORM"], "lsstcam") 

433 self.assertNotIn("LSST BUTLER ID", header) 

434 self.assertNotIn("AR HDU", header) 

435 self.assertNotIn("A_ORDER", header) 

436 # Check that the extension HDUs do not have any custom headers. 

437 self.assertFalse(self.visit_image._opaque_metadata.headers[ExtensionKey("IMAGE")]) 

438 self.assertFalse(self.visit_image._opaque_metadata.headers[ExtensionKey("MASK")]) 

439 self.assertFalse(self.visit_image._opaque_metadata.headers[ExtensionKey("VARIANCE")]) 

440 

441 def test_rewrite(self) -> None: 

442 """Test that we can rewrite the visit image and preserve both 

443 lossy-compressed pixel values and components exactly. 

444 """ 

445 with RoundtripFits(self, self.visit_image, "VisitImage") as roundtrip: 

446 # Check that we're still using the right compression, and that we 

447 # wrote WCSs. 

448 fits = roundtrip.inspect() 

449 self.assertEqual(fits[1].header["ZCMPTYPE"], "RICE_1") 

450 self.assertEqual(fits[1].header["CTYPE1"], "RA---TAN-SIP") 

451 self.assertEqual(fits[2].header["ZCMPTYPE"], "GZIP_2") 

452 self.assertEqual(fits[2].header["CTYPE1"], "RA---TAN-SIP") 

453 self.assertEqual(fits[3].header["ZCMPTYPE"], "RICE_1") 

454 self.assertEqual(fits[3].header["CTYPE1"], "RA---TAN-SIP") 

455 # Check a subimage read. 

456 subbox = Box.factory[8:13, 9:30] 

457 subimage = roundtrip.get(bbox=subbox) 

458 assert_masked_images_equal(self, subimage, self.visit_image[subbox], expect_view=False) 

459 alternates: dict[str, Any] = {} 

460 with self.subTest(): 

461 self.assertEqual(roundtrip.get("bbox"), self.visit_image.bbox) 

462 alternates = { 

463 k: roundtrip.get(k) 

464 for k in [ 

465 "projection", 

466 "image", 

467 "mask", 

468 "variance", 

469 "psf", 

470 "obs_info", 

471 "summary_stats", 

472 "aperture_corrections", 

473 "detector", 

474 ] 

475 } 

476 # Try to do a butler get of a component with storage class 

477 # override. 

478 with self.subTest(): 

479 if self.legacy_exposure is not None: 

480 import lsst.afw.image 

481 

482 # We have VisitInfo available. 

483 visit_info = roundtrip.get("obs_info", storageClass="VisitInfo") 

484 self.assertIsInstance(visit_info, lsst.afw.image.VisitInfo) 

485 self.assertEqual(visit_info.getInstrumentLabel(), "LSSTCam") 

486 else: 

487 raise unittest.SkipTest("Can not test VisitInfo conversion without afw") 

488 

489 assert_masked_images_equal(self, roundtrip.result, self.visit_image, expect_view=False) 

490 # Check that the round-tripped headers are the same (up to card order). 

491 self.assertEqual( 

492 dict(self.visit_image._opaque_metadata.headers[ExtensionKey()]), 

493 dict(roundtrip.result._opaque_metadata.headers[ExtensionKey()]), 

494 ) 

495 self.assertFalse(roundtrip.result._opaque_metadata.headers[ExtensionKey("IMAGE")]) 

496 self.assertFalse(roundtrip.result._opaque_metadata.headers[ExtensionKey("MASK")]) 

497 self.assertFalse(roundtrip.result._opaque_metadata.headers[ExtensionKey("VARIANCE")]) 

498 self.assertEqual(roundtrip.result._opaque_metadata.headers[ExtensionKey()]["PLATFORM"], "lsstcam") 

499 compare_visit_image_to_legacy( 

500 self, 

501 roundtrip.result, 

502 self.legacy_exposure, 

503 expect_view=False, 

504 plane_map=self.plane_map, 

505 **DP2_VISIT_DETECTOR_DATA_ID, 

506 alternates=alternates, 

507 ) 

508 # Check converting from the legacy object in-memory. 

509 compare_visit_image_to_legacy( 

510 self, 

511 VisitImage.from_legacy(self.legacy_exposure, plane_map=self.plane_map), 

512 self.legacy_exposure, 

513 expect_view=True, 

514 plane_map=self.plane_map, 

515 **DP2_VISIT_DETECTOR_DATA_ID, 

516 ) 

517 

518 def test_butler_converters(self) -> None: 

519 """Test that we can read a VisitImage and its components from a butler 

520 dataset written as an `lsst.afw.image.Exposure`. 

521 """ 

522 if self.legacy_exposure is None: 

523 raise unittest.SkipTest("lsst.afw.image.afw could not be imported.") 

524 with TemporaryButler(legacy="ExposureF") as helper: 

525 from lsst.daf.butler import FileDataset 

526 

527 helper.butler.ingest(FileDataset(path=self.filename, refs=[helper.legacy]), transfer="symlink") 

528 visit_image_ref = helper.legacy.overrideStorageClass("VisitImage") 

529 visit_image = helper.butler.get(visit_image_ref) 

530 bbox = helper.butler.get(visit_image_ref.makeComponentRef("bbox")) 

531 self.assertEqual(bbox, visit_image.bbox) 

532 alternates = { 

533 k: helper.butler.get(visit_image_ref.makeComponentRef(k)) 

534 # TODO: including "projection" or "obs_info" here fails because 

535 # there's code in daf_butler that expects any component to be 

536 # valid for the *internal* storage class, not the requested 

537 # one, and that's difficult to fix because it's tied up with 

538 # the data ID standardization logic. 

539 for k in ["image", "mask", "variance", "psf", "detector"] 

540 } 

541 compare_visit_image_to_legacy( 

542 self, 

543 visit_image, 

544 self.legacy_exposure, 

545 expect_view=False, 

546 plane_map=self.plane_map, 

547 alternates=alternates, 

548 **DP2_VISIT_DETECTOR_DATA_ID, 

549 ) 

550 

551 

552if __name__ == "__main__": 

553 unittest.main()