Coverage for tests / test_transforms.py: 26%
118 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-28 09:01 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-28 09:01 +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.
12from __future__ import annotations
14import dataclasses
15import functools
16import os
17import unittest
18from typing import Any
20import numpy as np
21import pydantic
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)
42DATA_DIR = os.environ.get("TESTDATA_IMAGES_DIR", None)
45class TransformTestCase(unittest.TestCase):
46 """Tests for the Transform, Projection, and FrameSet classes."""
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)
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:
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.
70 This test is skipped if legacy modules cannot be imported.
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 )
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
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 )
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 )
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 """
223 frames: CameraFrameSet
224 pixels_to_fp: Transform[DetectorFrame, FocalPlaneFrame]
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)
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)
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
251class FrameSetTestHolderModel[P: pydantic.BaseModel](ArchiveTree):
252 """The serialization model for FrameSetTestHolder."""
254 frames: CameraFrameSetSerializationModel | P
255 pixels_to_fp: TransformSerializationModel[P]
258if __name__ == "__main__":
259 unittest.main()