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