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