Coverage for tests / test_transforms.py: 26%

118 statements  

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

14import dataclasses 

15import functools 

16import os 

17import unittest 

18from typing import Any 

19 

20import numpy as np 

21import pydantic 

22 

23from lsst.images import ( 

24 Box, 

25 CameraFrameSet, 

26 CameraFrameSetSerializationModel, 

27 DetectorFrame, 

28 FocalPlaneFrame, 

29 Projection, 

30 Transform, 

31 TransformSerializationModel, 

32) 

33from lsst.images.serialization import ArchiveTree, InputArchive, OutputArchive, TableCellReferenceModel 

34from lsst.images.tests import ( 

35 DP2_VISIT_DETECTOR_DATA_ID, 

36 RoundtripFits, 

37 check_transform, 

38 compare_projection_to_legacy_wcs, 

39 legacy_points_to_xy_array, 

40) 

41 

42DATA_DIR = os.environ.get("TESTDATA_IMAGES_DIR", None) 

43 

44 

45class TransformTestCase(unittest.TestCase): 

46 """Tests for the Transform, Projection, and FrameSet classes.""" 

47 

48 def test_identity(self) -> None: 

49 """Test an identity transform.""" 

50 frame = DetectorFrame(**DP2_VISIT_DETECTOR_DATA_ID, bbox=Box.factory[:5, :4]) 

51 xy = frame.bbox.meshgrid().map(np.ravel) 

52 identity = Transform.identity(frame) 

53 check_transform(self, identity, xy, xy, frame, frame) 

54 self.assertEqual(identity.decompose(), []) 

55 with RoundtripFits(self, identity) as roundtrip: 

56 pass 

57 check_transform(self, roundtrip.result, xy, xy, frame, frame) 

58 

59 @unittest.skipUnless(DATA_DIR is not None, "TESTDATA_IMAGES_DIR is not in the environment.") 

60 def test_camera(self) -> None: 

61 """Test that we can: 

62 

63 - make a CameraFrameSet from the AST representation returned by afw; 

64 - transform points and get the same result as afw; 

65 - round-trip the CameraFrameSet through FITS serialization and still 

66 do all of that; 

67 - also roundtrip a Transform that can be obtained from the 

68 CameraFrameSet, by referencing the mappings in the frame set. 

69 

70 This test is skipped if legacy modules cannot be imported. 

71 

72 This test provides coverage for the archive system's pointer and 

73 frame-set reference machinery. 

74 """ 

75 try: 

76 from lsst.afw.cameraGeom import Camera 

77 except ImportError: 

78 raise unittest.SkipTest("'lsst.afw.cameraGeom' could not be imported.") from None 

79 assert DATA_DIR is not None, "Guaranteed by decorator." 

80 filename = os.path.join(DATA_DIR, "dp2", "legacy", "camera.fits") 

81 legacy_camera = Camera.readFits(filename) 

82 frame_set = CameraFrameSet.from_legacy(legacy_camera) 

83 detector_id: int = DP2_VISIT_DETECTOR_DATA_ID["detector"] 

84 self.compare_to_legacy_camera(legacy_camera, frame_set) 

85 test_holder = FrameSetTestHolder( 

86 frames=frame_set, 

87 pixels_to_fp=frame_set[frame_set.detector(detector_id), frame_set.focal_plane()], 

88 ) 

89 with RoundtripFits(self, test_holder) as roundtrip: 

90 self.assertEqual(len(roundtrip.serialized.pixels_to_fp.frames), 2) 

91 self.assertEqual(len(roundtrip.serialized.pixels_to_fp.bounds), 2) 

92 self.assertEqual(len(roundtrip.serialized.pixels_to_fp.mappings), 1) 

93 # Instead of storing the AST mapping directly, we should have 

94 # stored a reference to the frame set: 

95 self.assertIsInstance(roundtrip.serialized.pixels_to_fp.mappings[0], TableCellReferenceModel) 

96 self.compare_to_legacy_camera(legacy_camera, roundtrip.result.frames) 

97 self.assertEqual(roundtrip.result.pixels_to_fp.in_frame, frame_set.detector(detector_id)) 

98 self.assertEqual(roundtrip.result.pixels_to_fp.out_frame, frame_set.focal_plane()) 

99 self.assertEqual( 

100 roundtrip.result.pixels_to_fp._ast_mapping.simplified().show(), 

101 test_holder.pixels_to_fp._ast_mapping.simplified().show(), 

102 ) 

103 

104 def compare_to_legacy_camera(self, legacy_camera: Any, frame_set: CameraFrameSet) -> None: 

105 """Test the transforms extracted from a CameraFrameSet against the 

106 legacy lsst.afw.cameraGeom implementations. 

107 """ 

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

109 from lsst.geom import Point2D 

110 

111 legacy_detector = legacy_camera[16] 

112 pixel_legacy_points = [Point2D(50.0, 60.0), Point2D(801.2, 322.8), Point2D(33.5, 22.1)] 

113 fp_legacy_points = [legacy_detector.transform(p, PIXELS, FOCAL_PLANE) for p in pixel_legacy_points] 

114 fa_legacy_points = [legacy_detector.transform(p, PIXELS, FIELD_ANGLE) for p in pixel_legacy_points] 

115 pixel_xy_array = legacy_points_to_xy_array(pixel_legacy_points) 

116 fp_xy_array = legacy_points_to_xy_array(fp_legacy_points) 

117 fa_xy_array = legacy_points_to_xy_array(fa_legacy_points) 

118 # Test transforms extracted directly from the frame set. 

119 pixel_to_fp = frame_set[frame_set.detector(16), frame_set.focal_plane()] 

120 check_transform( 

121 self, pixel_to_fp, pixel_xy_array, fp_xy_array, frame_set.detector(16), frame_set.focal_plane() 

122 ) 

123 pixel_to_fa = frame_set[frame_set.detector(16), frame_set.field_angle()] 

124 check_transform( 

125 self, pixel_to_fa, pixel_xy_array, fa_xy_array, frame_set.detector(16), frame_set.field_angle() 

126 ) 

127 fp_to_fa = frame_set[frame_set.focal_plane(), frame_set.field_angle()] 

128 check_transform( 

129 self, fp_to_fa, fp_xy_array, fa_xy_array, frame_set.focal_plane(), frame_set.field_angle() 

130 ) 

131 # Test a composition. 

132 pixel_to_fa_indirect = pixel_to_fp.then(fp_to_fa) 

133 check_transform( 

134 self, 

135 pixel_to_fa_indirect, 

136 pixel_xy_array, 

137 fa_xy_array, 

138 frame_set.detector(16), 

139 frame_set.field_angle(), 

140 ) 

141 pixel_to_fp_d, fp_to_fa_d = pixel_to_fa_indirect.decompose() 

142 check_transform( 

143 self, pixel_to_fp_d, pixel_xy_array, fp_xy_array, frame_set.detector(16), frame_set.focal_plane() 

144 ) 

145 check_transform( 

146 self, fp_to_fa_d, fp_xy_array, fa_xy_array, frame_set.focal_plane(), frame_set.field_angle() 

147 ) 

148 fa_to_fp_d, fp_to_pixel_d = pixel_to_fa_indirect.inverted().decompose() 

149 check_transform( 

150 self, fa_to_fp_d, fa_xy_array, fp_xy_array, frame_set.field_angle(), frame_set.focal_plane() 

151 ) 

152 check_transform( 

153 self, fp_to_pixel_d, fp_xy_array, pixel_xy_array, frame_set.focal_plane(), frame_set.detector(16) 

154 ) 

155 

156 @unittest.skipUnless(DATA_DIR is not None, "TESTDATA_IMAGES_DIR is not in the environment.") 

157 def test_detector_wcs(self) -> None: 

158 """Test the Transform/Projection representation of a detector WCS.""" 

159 try: 

160 from lsst.afw.image import ExposureFitsReader 

161 except ImportError: 

162 raise unittest.SkipTest("'lsst.afw.image' could not be imported.") from None 

163 assert DATA_DIR is not None, "Guaranteed by decorator." 

164 filename = os.path.join(DATA_DIR, "dp2", "legacy", "visit_image.fits") 

165 reader = ExposureFitsReader(filename) 

166 legacy_wcs = reader.readWcs() 

167 wcs_bbox = Box.from_legacy(reader.readDetector().getBBox()) 

168 subimage_bbox = Box.from_legacy(reader.readBBox()) 

169 detector_frame = DetectorFrame(**DP2_VISIT_DETECTOR_DATA_ID, bbox=wcs_bbox) 

170 projection = Projection.from_legacy(legacy_wcs, detector_frame) 

171 assert projection.fits_approximation is not None 

172 compare_projection_to_legacy_wcs(self, projection, legacy_wcs, detector_frame, subimage_bbox) 

173 # When we convert from a legacy SkyWcs, the internal AST Mapping needs 

174 # to really be an AST FrameSet in order to be able to convert back. 

175 self.assertIn("Begin FrameSet", projection.show()) 

176 compare_projection_to_legacy_wcs( 

177 self, projection, projection.to_legacy(), detector_frame, subimage_bbox 

178 ) 

179 self.assertIn("Begin FrameSet", projection.fits_approximation.show()) 

180 compare_projection_to_legacy_wcs( 

181 self, 

182 projection.fits_approximation, 

183 projection.fits_approximation.to_legacy(), 

184 detector_frame, 

185 subimage_bbox, 

186 is_fits=True, 

187 ) 

188 with RoundtripFits(self, projection, "Projection") as roundtrip: 

189 pass 

190 compare_projection_to_legacy_wcs(self, roundtrip.result, legacy_wcs, detector_frame, subimage_bbox) 

191 # The AST FrameSet-ness needs to propagate through serialization. 

192 self.assertIn("Begin FrameSet", roundtrip.result.show()) 

193 compare_projection_to_legacy_wcs( 

194 self, projection, roundtrip.result.to_legacy(), detector_frame, subimage_bbox 

195 ) 

196 with RoundtripFits(self, projection.fits_approximation, "Projection") as roundtrip: 

197 pass 

198 compare_projection_to_legacy_wcs( 

199 self, 

200 roundtrip.result, 

201 legacy_wcs.getFitsApproximation(), 

202 detector_frame, 

203 subimage_bbox, 

204 is_fits=True, 

205 ) 

206 self.assertIn("Begin FrameSet", roundtrip.result.show()) 

207 compare_projection_to_legacy_wcs( 

208 self, 

209 projection.fits_approximation, 

210 roundtrip.result.to_legacy(), 

211 detector_frame, 

212 subimage_bbox, 

213 is_fits=True, 

214 ) 

215 

216 

217@dataclasses.dataclass 

218class FrameSetTestHolder: 

219 """A top-level object that holds a CameraFrameSet and a transform 

220 extracted from it, for testing archive pointers and frame set references. 

221 """ 

222 

223 frames: CameraFrameSet 

224 pixels_to_fp: Transform[DetectorFrame, FocalPlaneFrame] 

225 

226 def serialize[P: pydantic.BaseModel](self, archive: OutputArchive[P]) -> FrameSetTestHolderModel[P]: 

227 frames_model = archive.serialize_frame_set( 

228 "frames", self.frames, self.frames.serialize, key=id(self.frames) 

229 ) 

230 pixels_to_fp_model = archive.serialize_direct( 

231 "pixels_to_fp", functools.partial(self.pixels_to_fp.serialize, use_frame_sets=True) 

232 ) 

233 return FrameSetTestHolderModel[P](frames=frames_model, pixels_to_fp=pixels_to_fp_model) 

234 

235 @staticmethod 

236 def deserialize(model: FrameSetTestHolderModel[Any], archive: InputArchive[Any]) -> FrameSetTestHolder: 

237 assert not isinstance(model.frames, CameraFrameSetSerializationModel), "Archive pointer expected." 

238 frames = archive.deserialize_pointer( 

239 model.frames, CameraFrameSetSerializationModel, CameraFrameSet.deserialize 

240 ) 

241 pixels_to_fp = Transform.deserialize(model.pixels_to_fp, archive) 

242 return FrameSetTestHolder(frames, pixels_to_fp) 

243 

244 @staticmethod 

245 def _get_archive_tree_type[P: pydantic.BaseModel]( 

246 pointer_type: type[P], 

247 ) -> type[FrameSetTestHolderModel[P]]: 

248 return FrameSetTestHolderModel[pointer_type] # type: ignore 

249 

250 

251class FrameSetTestHolderModel[P: pydantic.BaseModel](ArchiveTree): 

252 """The serialization model for FrameSetTestHolder.""" 

253 

254 frames: CameraFrameSetSerializationModel | P 

255 pixels_to_fp: TransformSerializationModel[P] 

256 

257 

258if __name__ == "__main__": 

259 unittest.main()