Coverage for python / lsst / images / _transforms / _camera_frame_set.py: 26%
97 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-22 09:12 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-22 09:12 +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
14__all__ = ("CameraFrameSet", "CameraFrameSetSerializationModel")
16from typing import Any
18import astropy.units as u
19import pydantic
21from .._geom import Bounds, Box
22from ..serialization import ArchiveTree, InputArchive, OutputArchive
23from . import _ast as astshim
24from . import _frames # use this import style to facilitate pattern matching
25from ._frame_set import FrameLookupError, FrameSet
26from ._transform import Transform
29class CameraFrameSet(FrameSet):
30 """A `FrameSet` that manages the coordinate systems of a camera.
32 The `CameraFrameSet` class constructor is considered a private
33 implementation detail. At present, instances can only be obtained by
34 loading them from storage (`deserialize`) or converting a legacy
35 `lsst.afw.cameraGeom` object (`from_legacy`).
36 """
38 # This constructor is kept private while we support both the astshim
39 # and starlink-pyast AST wrappers. For now:
40 # 'instrument': the short (butler dimension) name.
41 # 'ast': an astshim.FrameSet as returned by
42 # lsst.afw.cameraGeom.TransformMap.makeFrameSet.
43 # Should have frames with Ident values FOCAL_PLANE, FIELD_ANGLE
44 # and DETECTOR_${ID}, and the focal plane frame must know its
45 # units.
46 def __init__(self, instrument: str, ast: astshim.FrameSet):
47 self._ast = ast
48 self._focal_plane_frame_id: int = 0
49 self._field_angle_frame_id: int = 0
50 self._detector_frame_ids: dict[int, int] = {}
51 for frame_id in range(1, self._ast.nFrame + 1):
52 ast_frame = self._ast.getFrame(frame_id, copy=False)
53 match ast_frame.ident:
54 case "FOCAL_PLANE":
55 self._focal_plane_frame_id = frame_id
56 case "FIELD_ANGLE":
57 self._field_angle_frame_id = frame_id
58 case str(s) if s.startswith("DETECTOR_"):
59 detector_id = int(s.removeprefix("DETECTOR_"))
60 self._detector_frame_ids[detector_id] = frame_id
61 case _:
62 raise ValueError(f"Unexpected frame in camera AST FrameSet:\n{ast_frame.show()}.")
63 if self._focal_plane_frame_id == 0:
64 raise ValueError("No FOCAL_PLANE frame in camera AST FrameSet.")
65 self._focal_plane_frame = _frames.FocalPlaneFrame(
66 instrument=instrument,
67 unit=u.Unit(self._ast.getFrame(self._focal_plane_frame_id, copy=False).getUnit(1)),
68 )
69 self._field_angle_frame = _frames.FieldAngleFrame(instrument=instrument)
70 if self._field_angle_frame_id == 0:
71 raise ValueError("No FIELD_ANGLE frame in camera AST FrameSet.")
73 @property
74 def instrument(self) -> str:
75 """Name of the instrument (`str`)."""
76 return self._focal_plane_frame.instrument
78 def focal_plane(self, visit: int | None = None) -> _frames.FocalPlaneFrame:
79 """Return a focal plane frame for this instrument.
81 Parameters
82 ----------
83 visit
84 ID for the visit this frame will correspond to. This only needs
85 to be provided in contexts where camera frames will be related to
86 the sky via a `Projection`.
87 """
88 if visit is None:
89 return self._focal_plane_frame
90 else:
91 return self._focal_plane_frame.model_copy(update={"visit": visit})
93 def field_angle(self, visit: int | None = None) -> _frames.FieldAngleFrame:
94 """Return a field angle frame for this instrument.
96 Parameters
97 ----------
98 visit
99 ID for the visit this frame will correspond to. This only needs
100 to be provided in contexts where camera frames will be related to
101 the sky via a `Projection`.
102 """
103 if visit is None:
104 return self._field_angle_frame
105 else:
106 return self._field_angle_frame.model_copy(update={"visit": visit})
108 def detector(self, detector: int, *, visit: int | None = None) -> _frames.DetectorFrame:
109 """Return a detector pixel-coordinate frame for this instrument.
111 Parameters
112 ----------
113 detector
114 ID of the detector.
115 visit
116 ID for the visit this frame will correspond to. This only needs
117 to be provided in contexts where camera frames will be related to
118 the sky via a `Projection`.
119 """
120 try:
121 frame_id = self._detector_frame_ids[detector]
122 except KeyError:
123 raise FrameLookupError(
124 f"No frame for detector {detector!r} in camera for {self.instrument!r}."
125 ) from None
126 ast_frame = self._ast.getFrame(frame_id, copy=False)
127 bbox = Box.factory[
128 int(ast_frame.getBottom(2)) : int(ast_frame.getTop(2)),
129 int(ast_frame.getBottom(1)) : int(ast_frame.getTop(1)),
130 ]
131 return _frames.DetectorFrame(instrument=self.instrument, detector=detector, visit=visit, bbox=bbox)
133 def __contains__(self, frame: _frames.Frame) -> bool:
134 try:
135 self._parse_frame_arg(frame)
136 return True
137 except FrameLookupError:
138 return False
140 def __getitem__[I: _frames.Frame, O: _frames.Frame](self, key: tuple[I, O]) -> Transform[I, O]:
141 in_frame, out_frame = key
142 in_frame_id, in_bounds = self._parse_frame_arg(in_frame)
143 out_frame_id, out_bounds = self._parse_frame_arg(out_frame)
144 return Transform(
145 in_frame,
146 out_frame,
147 self._ast.getMapping(in_frame_id, out_frame_id),
148 in_bounds=in_bounds,
149 out_bounds=out_bounds,
150 )
152 def _parse_frame_arg(self, frame: _frames.Frame) -> tuple[int, Bounds | None]:
153 bounds: Bounds | None = None
154 match frame:
155 case _frames.DetectorFrame(instrument=self.instrument, detector=detector_id):
156 try:
157 frame_id = self._detector_frame_ids[detector_id]
158 except KeyError:
159 raise FrameLookupError(
160 f"No frame for detector {detector_id!r} in camera for {self.instrument!r}."
161 ) from None
162 bounds = frame.bbox
163 case _frames.FocalPlaneFrame(instrument=self.instrument):
164 frame_id = self._focal_plane_frame_id
165 case _frames.FieldAngleFrame(instrument=self.instrument):
166 frame_id = self._field_angle_frame_id
167 case _:
168 raise FrameLookupError(f"Invalid frame for camera {self.instrument}: {frame!r}.")
169 return frame_id, bounds
171 def serialize(self, archive: OutputArchive[Any]) -> CameraFrameSetSerializationModel:
172 """Serialize the frame set to an archive.
174 Parameters
175 ----------
176 archive
177 Archive to serialize to.
179 Returns
180 -------
181 `CameraFrameSetSerializationModel`
182 Serialized form of the frame set.
183 """
184 return CameraFrameSetSerializationModel(instrument=self.instrument, ast=self._ast.show())
186 @staticmethod
187 def deserialize(model: CameraFrameSetSerializationModel, archive: InputArchive[Any]) -> CameraFrameSet:
188 """Deserialize a frame set from an archive.
190 Parameters
191 ----------
192 model
193 Seralized form of the frame set.
194 archive
195 Archive to read from.
196 """
197 return CameraFrameSet(model.instrument, astshim.FrameSet.fromString(model.ast))
199 @staticmethod
200 def _get_archive_tree_type(
201 pointer_type: type[pydantic.BaseModel],
202 ) -> type[CameraFrameSetSerializationModel]:
203 """Return the serialization model type for this object for an archive
204 type that uses the given pointer type.
205 """
206 return CameraFrameSetSerializationModel
208 @classmethod
209 def from_legacy(cls, camera: Any) -> CameraFrameSet:
210 """Construct a transform from a legacy `lsst.afw.cameraGeom.Camera`.
212 Parameters
213 ----------
214 camera
215 An `lsst.afw.cameraGeom.Camera` instance to convert.
216 """
217 transform_map = camera.getTransformMap()
218 ast_frame_set = transform_map.makeFrameSet(list(camera))
219 return CameraFrameSet("HSC", ast_frame_set)
222class CameraFrameSetSerializationModel(ArchiveTree):
223 """Serialization model for `CameraFrameSet`."""
225 instrument: str = pydantic.Field(description="Name of the instrument.")
226 ast: str = pydantic.Field(
227 description="A serialized Starlink AST FrameSet, using the AST native encoding."
228 )