Coverage for tests/test_cameras.py: 55%

86 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-23 03:09 -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 

15from typing import Any 

16 

17import pytest 

18 

19from lsst.images import YX 

20from lsst.images.cameras import AmplifierRawGeometry, Detector, ReadoutCorner 

21from lsst.images.describe import DescribableMixin, Report 

22from lsst.images.serialization import read_archive 

23from lsst.images.tests import ( 

24 DP2_VISIT_DETECTOR_DATA_ID, 

25 RoundtripFits, 

26 compare_detector_to_legacy, 

27 reset_afw_mask_planes, # noqa: F401 

28) 

29 

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

31 

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

33 

34 

35@pytest.fixture 

36def legacy_camera_data(reset_afw_mask_planes: None) -> dict[str, Any]: # noqa: F811 

37 """Return a dict with legacy_camera and legacy_detector loaded from the 

38 test data directory. 

39 """ 

40 if EXTERNAL_DATA_DIR is None: 40 ↛ 43line 40 didn't jump to line 43 because the condition on line 40 was always true

41 pytest.skip("TESTDATA_IMAGES_DIR is not in the environment.") 

42 # reset_afw_mask_planes will have already skipped if afw is not available. 

43 from lsst.afw.cameraGeom import Camera as LegacyCamera 

44 from lsst.afw.image import ExposureFitsReader 

45 

46 legacy_camera = LegacyCamera.readFits(os.path.join(EXTERNAL_DATA_DIR, "dp2", "legacy", "camera.fits")) 

47 legacy_detector = ExposureFitsReader( 

48 os.path.join(EXTERNAL_DATA_DIR, "dp2", "legacy", "visit_image.fits") 

49 ).readDetector() 

50 return {"legacy_camera": legacy_camera, "legacy_detector": legacy_detector} 

51 

52 

53def test_visit_image_detector_legacy_conversions(legacy_camera_data: dict[str, Any]) -> None: 

54 """Test converting a visit-image detector from legacy and back with 

55 serialization in between. 

56 """ 

57 legacy_detector = legacy_camera_data["legacy_detector"] 

58 detector = Detector.from_legacy( 

59 legacy_detector, 

60 instrument=DP2_VISIT_DETECTOR_DATA_ID["instrument"], 

61 visit=DP2_VISIT_DETECTOR_DATA_ID["visit"], 

62 ) 

63 compare_detector_to_legacy(detector, legacy_detector, is_raw_assembled=True) 

64 with RoundtripFits(detector) as roundtrip: 

65 pass 

66 compare_detector_to_legacy(roundtrip.result, legacy_detector, is_raw_assembled=True) 

67 compare_detector_to_legacy(detector, roundtrip.result.to_legacy(), is_raw_assembled=True) 

68 

69 

70def test_camera_detector_legacy_conversions(legacy_camera_data: dict[str, Any]) -> None: 

71 """Test converting one detector of each type from a legacy Camera and 

72 back. 

73 """ 

74 legacy_camera = legacy_camera_data["legacy_camera"] 

75 detector_types_seen = set() 

76 for legacy_detector_1 in legacy_camera: 

77 if legacy_detector_1.getType() in detector_types_seen: 

78 continue 

79 detector_types_seen.add(legacy_detector_1.getType()) 

80 detector = Detector.from_legacy( 

81 legacy_detector_1, 

82 instrument=DP2_VISIT_DETECTOR_DATA_ID["instrument"], 

83 ) 

84 compare_detector_to_legacy(detector, legacy_detector_1, is_raw_assembled=False) 

85 with RoundtripFits(detector) as roundtrip: 

86 pass 

87 compare_detector_to_legacy(roundtrip.result, legacy_detector_1, is_raw_assembled=False) 

88 legacy_detector_2 = roundtrip.result.to_legacy() 

89 compare_detector_to_legacy(detector, legacy_detector_2, is_raw_assembled=False) 

90 

91 

92def test_expanded_detector_roundtrip(legacy_camera_data: dict[str, Any]) -> None: 

93 """Test round-tripping a detector that holds both assembled and 

94 unassembled raw amplifier geometry. 

95 """ 

96 legacy_camera = legacy_camera_data["legacy_camera"] 

97 legacy_detector = legacy_camera_data["legacy_detector"] 

98 legacy_camera_detector = legacy_camera[DP2_VISIT_DETECTOR_DATA_ID["detector"]] 

99 detector = Detector.from_legacy( 

100 legacy_camera_detector, 

101 instrument=DP2_VISIT_DETECTOR_DATA_ID["instrument"], 

102 visit=DP2_VISIT_DETECTOR_DATA_ID["visit"], 

103 is_raw_assembled=False, 

104 ) 

105 for amplifier, legacy_assembled_amplifier in zip( 

106 detector.amplifiers, legacy_detector.getAmplifiers(), strict=True 

107 ): 

108 assert amplifier.unassembled_raw_geometry is not None 

109 assert amplifier.assembled_raw_geometry is None 

110 amplifier.assembled_raw_geometry = AmplifierRawGeometry.from_legacy_amplifier( 

111 legacy_assembled_amplifier 

112 ) 

113 with RoundtripFits(detector) as roundtrip: 

114 pass 

115 compare_detector_to_legacy(roundtrip.result, legacy_camera_detector, is_raw_assembled=False) 

116 compare_detector_to_legacy(roundtrip.result, legacy_detector, is_raw_assembled=True) 

117 

118 

119def test_as_flips() -> None: 

120 """Tes that ReadoutCorner.as_flips returns the correct YX flip flags for 

121 each corner. 

122 """ 

123 assert ReadoutCorner.LL.as_flips() == YX(y=False, x=False) 

124 assert ReadoutCorner.LR.as_flips() == YX(y=False, x=True) 

125 assert ReadoutCorner.UL.as_flips() == YX(y=True, x=False) 

126 assert ReadoutCorner.UR.as_flips() == YX(y=True, x=True) 

127 

128 

129def test_flips_roundtrip() -> None: 

130 """Test that ReadoutCorner.from_flips is the inverse of as_flips for all 

131 corners. 

132 """ 

133 for corner in ReadoutCorner: 

134 flips = corner.as_flips() 

135 assert ReadoutCorner.from_flips(y=flips.y, x=flips.x) is corner 

136 

137 

138def test_apply_flips() -> None: 

139 """Test that ReadoutCorner.apply_flips with identity returns the same 

140 corner, and a few specific flips are correct. 

141 """ 

142 for corner in ReadoutCorner: 

143 assert corner.apply_flips(y=False, x=False) is corner 

144 assert ReadoutCorner.LL.apply_flips(y=True, x=True) is ReadoutCorner.UR 

145 assert ReadoutCorner.LR.apply_flips(y=False, x=True) is ReadoutCorner.LL 

146 assert ReadoutCorner.UL.apply_flips(y=True, x=False) is ReadoutCorner.LL 

147 assert ReadoutCorner.UR.apply_flips(y=True, x=True) is ReadoutCorner.LL 

148 

149 

150def test_detector_describe() -> None: 

151 """Detector._describe returns a Report with the expected fields.""" 

152 detector = read_archive(os.path.join(LOCAL_DATA_DIR, "detector.json"), Detector) 

153 assert isinstance(detector, DescribableMixin) 

154 report = detector._describe() 

155 assert isinstance(report, Report) 

156 assert report.type_name == "Detector" 

157 labels = {f.label for f in report.fields} 

158 assert "instrument" in labels 

159 assert "name" in labels 

160 assert "id" in labels 

161 assert "type" in labels 

162 assert "serial" in labels 

163 assert "bbox" in labels 

164 name_field = next(f for f in report.fields if f.label == "name") 

165 assert name_field.value == detector.name