Coverage for tests / test_transforms.py: 29%
106 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-06 01:14 -0700
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-06 01:14 -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.
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(), [])
56 @unittest.skipUnless(DATA_DIR is not None, "TESTDATA_IMAGES_DIR is not in the environment.")
57 def test_camera(self) -> None:
58 """Test that we can:
60 - make a CameraFrameSet from the AST representation returned by afw;
61 - transform points and get the same result as afw;
62 - round-trip the CameraFrameSet through FITS serialization and still
63 do all of that;
64 - also roundtrip a Transform that can be obtained from the
65 CameraFrameSet, by referencing the mappings in the frame set.
67 This test is skipped if legacy modules cannot be imported.
69 This test provides coverage for the archive system's pointer and
70 frame-set reference machinery.
71 """
72 try:
73 from lsst.afw.cameraGeom import Camera
74 except ImportError:
75 raise unittest.SkipTest("'lsst.afw.cameraGeom' could not be imported.") from None
76 assert DATA_DIR is not None, "Guaranteed by decorator."
77 filename = os.path.join(DATA_DIR, "dp2", "legacy", "camera.fits")
78 legacy_camera = Camera.readFits(filename)
79 frame_set = CameraFrameSet.from_legacy(legacy_camera)
80 detector_id: int = DP2_VISIT_DETECTOR_DATA_ID["detector"]
81 self.compare_to_legacy_camera(legacy_camera, frame_set)
82 test_holder = FrameSetTestHolder(
83 frames=frame_set,
84 pixels_to_fp=frame_set[frame_set.detector(detector_id), frame_set.focal_plane()],
85 )
86 with RoundtripFits(self, test_holder) as roundtrip:
87 self.assertEqual(len(roundtrip.serialized.pixels_to_fp.frames), 2)
88 self.assertEqual(len(roundtrip.serialized.pixels_to_fp.bounds), 2)
89 self.assertEqual(len(roundtrip.serialized.pixels_to_fp.mappings), 1)
90 # Instead of storing the AST mapping directly, we should have
91 # stored a reference to the frame set:
92 self.assertIsInstance(roundtrip.serialized.pixels_to_fp.mappings[0], TableCellReferenceModel)
93 self.compare_to_legacy_camera(legacy_camera, roundtrip.result.frames)
94 self.assertEqual(roundtrip.result.pixels_to_fp.in_frame, frame_set.detector(detector_id))
95 self.assertEqual(roundtrip.result.pixels_to_fp.out_frame, frame_set.focal_plane())
96 self.assertEqual(
97 roundtrip.result.pixels_to_fp._ast_mapping.simplified().show(),
98 test_holder.pixels_to_fp._ast_mapping.simplified().show(),
99 )
101 def compare_to_legacy_camera(self, legacy_camera: Any, frame_set: CameraFrameSet) -> None:
102 """Test the transforms extracted from a CameraFrameSet against the
103 legacy lsst.afw.cameraGeom implementations.
104 """
105 from lsst.afw.cameraGeom import FIELD_ANGLE, FOCAL_PLANE, PIXELS
106 from lsst.geom import Point2D
108 legacy_detector = legacy_camera[16]
109 pixel_legacy_points = [Point2D(50.0, 60.0), Point2D(801.2, 322.8), Point2D(33.5, 22.1)]
110 fp_legacy_points = [legacy_detector.transform(p, PIXELS, FOCAL_PLANE) for p in pixel_legacy_points]
111 fa_legacy_points = [legacy_detector.transform(p, PIXELS, FIELD_ANGLE) for p in pixel_legacy_points]
112 pixel_xy_array = legacy_points_to_xy_array(pixel_legacy_points)
113 fp_xy_array = legacy_points_to_xy_array(fp_legacy_points)
114 fa_xy_array = legacy_points_to_xy_array(fa_legacy_points)
115 # Test transforms extracted directly from the frame set.
116 pixel_to_fp = frame_set[frame_set.detector(16), frame_set.focal_plane()]
117 check_transform(
118 self, pixel_to_fp, pixel_xy_array, fp_xy_array, frame_set.detector(16), frame_set.focal_plane()
119 )
120 pixel_to_fa = frame_set[frame_set.detector(16), frame_set.field_angle()]
121 check_transform(
122 self, pixel_to_fa, pixel_xy_array, fa_xy_array, frame_set.detector(16), frame_set.field_angle()
123 )
124 fp_to_fa = frame_set[frame_set.focal_plane(), frame_set.field_angle()]
125 check_transform(
126 self, fp_to_fa, fp_xy_array, fa_xy_array, frame_set.focal_plane(), frame_set.field_angle()
127 )
128 # Test a composition.
129 pixel_to_fa_indirect = pixel_to_fp.then(fp_to_fa)
130 check_transform(
131 self,
132 pixel_to_fa_indirect,
133 pixel_xy_array,
134 fa_xy_array,
135 frame_set.detector(16),
136 frame_set.field_angle(),
137 )
138 pixel_to_fp_d, fp_to_fa_d = pixel_to_fa_indirect.decompose()
139 check_transform(
140 self, pixel_to_fp_d, pixel_xy_array, fp_xy_array, frame_set.detector(16), frame_set.focal_plane()
141 )
142 check_transform(
143 self, fp_to_fa_d, fp_xy_array, fa_xy_array, frame_set.focal_plane(), frame_set.field_angle()
144 )
145 fa_to_fp_d, fp_to_pixel_d = pixel_to_fa_indirect.inverted().decompose()
146 check_transform(
147 self, fa_to_fp_d, fa_xy_array, fp_xy_array, frame_set.field_angle(), frame_set.focal_plane()
148 )
149 check_transform(
150 self, fp_to_pixel_d, fp_xy_array, pixel_xy_array, frame_set.focal_plane(), frame_set.detector(16)
151 )
153 @unittest.skipUnless(DATA_DIR is not None, "TESTDATA_IMAGES_DIR is not in the environment.")
154 def test_detector_wcs(self) -> None:
155 """Test the Transform/Projection representation of a detector WCS."""
156 try:
157 from lsst.afw.image import ExposureFitsReader
158 except ImportError:
159 raise unittest.SkipTest("'lsst.afw.image' could not be imported.") from None
160 assert DATA_DIR is not None, "Guaranteed by decorator."
161 filename = os.path.join(DATA_DIR, "dp2", "legacy", "visit_image.fits")
162 reader = ExposureFitsReader(filename)
163 legacy_wcs = reader.readWcs()
164 wcs_bbox = Box.from_legacy(reader.readDetector().getBBox())
165 subimage_bbox = Box.from_legacy(reader.readBBox())
166 detector_frame = DetectorFrame(**DP2_VISIT_DETECTOR_DATA_ID, bbox=wcs_bbox)
167 projection = Projection.from_legacy(legacy_wcs, detector_frame)
168 compare_projection_to_legacy_wcs(self, projection, legacy_wcs, detector_frame, subimage_bbox)
169 with RoundtripFits(self, projection, "Projection") as roundtrip:
170 pass
171 compare_projection_to_legacy_wcs(self, roundtrip.result, legacy_wcs, detector_frame, subimage_bbox)
172 with RoundtripFits(self, projection.fits_approximation, "Projection") as roundtrip:
173 pass
174 compare_projection_to_legacy_wcs(
175 self,
176 roundtrip.result,
177 legacy_wcs.getFitsApproximation(),
178 detector_frame,
179 subimage_bbox,
180 is_fits=True,
181 )
184@dataclasses.dataclass
185class FrameSetTestHolder:
186 """A top-level object that holds a CameraFrameSet and a transform
187 extracted from it, for testing archive pointers and frame set references.
188 """
190 frames: CameraFrameSet
191 pixels_to_fp: Transform[DetectorFrame, FocalPlaneFrame]
193 def serialize[P: pydantic.BaseModel](self, archive: OutputArchive[P]) -> FrameSetTestHolderModel[P]:
194 frames_model = archive.serialize_frame_set(
195 "frames", self.frames, self.frames.serialize, key=id(self.frames)
196 )
197 pixels_to_fp_model = archive.serialize_direct(
198 "pixels_to_fp", functools.partial(self.pixels_to_fp.serialize, use_frame_sets=True)
199 )
200 return FrameSetTestHolderModel[P](frames=frames_model, pixels_to_fp=pixels_to_fp_model)
202 @staticmethod
203 def deserialize(model: FrameSetTestHolderModel[Any], archive: InputArchive[Any]) -> FrameSetTestHolder:
204 assert not isinstance(model.frames, CameraFrameSetSerializationModel), "Archive pointer expected."
205 frames = archive.deserialize_pointer(
206 model.frames, CameraFrameSetSerializationModel, CameraFrameSet.deserialize
207 )
208 pixels_to_fp = Transform.deserialize(model.pixels_to_fp, archive)
209 return FrameSetTestHolder(frames, pixels_to_fp)
211 @staticmethod
212 def _get_archive_tree_type[P: pydantic.BaseModel](
213 pointer_type: type[P],
214 ) -> type[FrameSetTestHolderModel[P]]:
215 return FrameSetTestHolderModel[pointer_type] # type: ignore
218class FrameSetTestHolderModel[P: pydantic.BaseModel](ArchiveTree):
219 """The serialization model for FrameSetTestHolder."""
221 frames: CameraFrameSetSerializationModel | P
222 pixels_to_fp: TransformSerializationModel[P]
225if __name__ == "__main__":
226 unittest.main()