Coverage for python / lsst / images / _transforms / _frames.py: 71%
149 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-01 08:36 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-01 08:36 +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__ = (
15 "ICRS",
16 "DetectorFrame",
17 "FieldAngleFrame",
18 "FocalPlaneFrame",
19 "Frame",
20 "GeneralFrame",
21 "SerializableFrame",
22 "SkyFrame",
23 "TractFrame",
24)
26import enum
27from typing import Annotated, Literal, Protocol, Self, cast, final
29import astropy.units as u
30import numpy as np
31import pydantic
33from .._geom import Box
34from ..serialization import ArchiveTree, Unit
35from ..utils import is_none
38class Frame(Protocol):
39 """A description of a coordinate system."""
41 @property
42 def unit(self) -> u.UnitBase:
43 """Units of the coordinates in this frame
44 (`astropy.units.UnitBase`).
45 """
46 ...
48 def standardize_x[T: float | np.ndarray](self, x: T) -> T:
49 """Coerce ``x`` coordinates into their standard range."""
50 ...
52 def standardize_y[T: float | np.ndarray](self, y: T) -> T:
53 """Coerce ``y`` coordinates into their standard range."""
54 ...
56 # At present all Frames are members of the same serializable Union,
57 # so their serialized form is just the original frame. But this may not
58 # always be the case.
60 def serialize(self) -> SerializableFrame:
61 """Return a Pydantic-serializable version of this Frame."""
62 ...
64 @classmethod
65 def deserialize(cls, serialized: SerializableFrame) -> Self:
66 """Convert a serialized frame to an in-memory one."""
67 return cast(Self, serialized)
69 @property
70 def _ast_ident(self) -> str:
71 """String to use as the 'Ident' attribute of an AST Frame."""
72 ...
75@final
76class DetectorFrame(ArchiveTree, frozen=True):
77 """A coordinate frame for a particular detector's pixels.
79 Notes
80 -----
81 This frame is only used for post-assembly images (i.e. not those with
82 overscan regions still present).
83 """
85 instrument: str = pydantic.Field(description="Name of the instrument.")
86 visit: int | None = pydantic.Field(
87 default=None,
88 description=(
89 "ID of the visit. May be unset in contexts where there "
90 "is no visit or only a single relevant visit."
91 ),
92 exclude_if=is_none,
93 )
94 detector: int = pydantic.Field(description="ID of the detector.")
95 bbox: Box = pydantic.Field(description="Bounding box of the detector.")
96 frame_type: Literal["DETECTOR"] = pydantic.Field(
97 default="DETECTOR", description="Discriminator for the frame type."
98 )
100 @property
101 def unit(self) -> u.UnitBase:
102 """Units of the coordinates in this frame
103 (`astropy.units.UnitBase`).
104 """
105 return u.pix
107 def standardize_x[T: float | np.ndarray](self, x: T) -> T:
108 """Coerce ``x`` coordinates into their standard range."""
109 return x
111 def standardize_y[T: float | np.ndarray](self, y: T) -> T:
112 """Coerce ``y`` coordinates into their standard range."""
113 return y
115 def serialize(self) -> SerializableFrame:
116 """Return a Pydantic-serializable version of this Frame."""
117 return cast(SerializableFrame, self)
119 @classmethod
120 def deserialize(cls, serialized: SerializableFrame) -> Self:
121 """Convert a serialized frame to an in-memory one."""
122 return cast(Self, serialized)
124 @property
125 def _ast_ident(self) -> str:
126 return f"{_camera_ast_ident(self.instrument, self.visit)}/DETECTOR_{self.detector:03d}"
129@final
130class FocalPlaneFrame(ArchiveTree, frozen=True):
131 """A Euclidean coordinate frame for the focal plane of a camera."""
133 instrument: str = pydantic.Field(description="Name of the instrument.")
134 visit: int | None = pydantic.Field(
135 default=None,
136 description=(
137 "ID of the visit. May be unset in contexts where there "
138 "is no visit or only a relevant single visit."
139 ),
140 exclude_if=is_none,
141 )
142 unit: Unit = pydantic.Field(description="Units of the coordinates in this frame.")
144 frame_type: Literal["FOCAL_PLANE"] = pydantic.Field(
145 default="FOCAL_PLANE", description="Discriminator for the frame type."
146 )
148 def standardize_x[T: float | np.ndarray](self, x: T) -> T:
149 """Coerce ``x`` coordinates into their standard range."""
150 return x
152 def standardize_y[T: float | np.ndarray](self, y: T) -> T:
153 """Coerce ``y`` coordinates into their standard range."""
154 return y
156 def serialize(self) -> SerializableFrame:
157 """Return a Pydantic-serializable version of this Frame."""
158 return cast(SerializableFrame, self)
160 @classmethod
161 def deserialize(cls, serialized: SerializableFrame) -> Self:
162 """Convert a serialized frame to an in-memory one."""
163 return cast(Self, serialized)
165 @property
166 def _ast_ident(self) -> str:
167 return f"{_camera_ast_ident(self.instrument, self.visit)}/FOCAL_PLANE"
170@final
171class FieldAngleFrame(ArchiveTree, frozen=True):
172 """An angular coordinate frame that maps a camera onto the sky about its
173 boresight.
175 Notes
176 -----
177 The transform between a `FocalPlaneFrame` and a `FieldAngleFrame` includes
178 optical distortions but no rotation. It may include a parity flip.
179 """
181 instrument: str = pydantic.Field(description="Name of the instrument.")
182 visit: int | None = pydantic.Field(
183 default=None,
184 description=(
185 "ID of the visit. May be unset in contexts where there "
186 "is no visit or only a relevant single visit."
187 ),
188 exclude_if=is_none,
189 )
190 frame_type: Literal["FIELD_ANGLE"] = pydantic.Field(
191 default="FIELD_ANGLE", description="Discriminator for the frame type."
192 )
194 @property
195 def unit(self) -> u.UnitBase:
196 """Units of the coordinates in this frame
197 (`astropy.units.UnitBase`).
198 """
199 return u.rad
201 def standardize_x[T: float | np.ndarray](self, x: T) -> T:
202 """Coerce ``x`` coordinates into their standard range."""
203 return _wrap_symmetric(x)
205 def standardize_y[T: float | np.ndarray](self, y: T) -> T:
206 """Coerce ``y`` coordinates into their standard range."""
207 return _wrap_symmetric(y)
209 def serialize(self) -> SerializableFrame:
210 """Return a Pydantic-serializable version of this Frame."""
211 return cast(SerializableFrame, self)
213 @classmethod
214 def deserialize(cls, serialized: SerializableFrame) -> Self:
215 """Convert a serialized frame to an in-memory one."""
216 return cast(Self, serialized)
218 @property
219 def _ast_ident(self) -> str:
220 return f"{_camera_ast_ident(self.instrument, self.visit)}/FIELD_ANGLE"
223@final
224class TractFrame(ArchiveTree, frozen=True):
225 """The pixel coordinates of a tract: a region on the sky used for
226 coaddition, defined by a 'skymap' and split into 'patches' that share
227 a common pixel grid.
228 """
230 skymap: str = pydantic.Field(description="Name of the skymap.")
231 tract: int = pydantic.Field(description="ID of the tract within its skymap.")
232 bbox: Box = pydantic.Field(description="Bounding box of the full tract.")
233 frame_type: Literal["TRACT"] = pydantic.Field(
234 default="TRACT", description="Discriminator for the frame type."
235 )
237 @property
238 def unit(self) -> u.UnitBase:
239 """Units of the coordinates in this frame
240 (`astropy.units.UnitBase`).
241 """
242 return u.pix
244 def standardize_x[T: float | np.ndarray](self, x: T) -> T:
245 """Coerce ``x`` coordinates into their standard range."""
246 return x
248 def standardize_y[T: float | np.ndarray](self, y: T) -> T:
249 """Coerce ``y`` coordinates into their standard range."""
250 return y
252 def serialize(self) -> SerializableFrame:
253 """Return a Pydantic-serializable version of this Frame."""
254 return cast(SerializableFrame, self)
256 @classmethod
257 def deserialize(cls, serialized: SerializableFrame) -> Self:
258 """Convert a serialized frame to an in-memory one."""
259 return cast(Self, serialized)
261 @property
262 def _ast_ident(self) -> str:
263 return f"{self.skymap}@{self.tract}"
266@final
267class GeneralFrame(ArchiveTree, frozen=True):
268 """An arbitrary Euclidean coordinate system."""
270 unit: Unit = pydantic.Field(description="Units of the coordinates in this frame.")
272 frame_type: Literal["GENERAL"] = pydantic.Field(
273 default="GENERAL", description="Discriminator for the frame type."
274 )
276 def standardize_x[T: float | np.ndarray](self, x: T) -> T:
277 """Coerce ``x`` coordinates into their standard range."""
278 return x
280 def standardize_y[T: float | np.ndarray](self, y: T) -> T:
281 """Coerce ``y`` coordinates into their standard range."""
282 return y
284 def serialize(self) -> SerializableFrame:
285 """Return a Pydantic-serializable version of this Frame."""
286 return cast(SerializableFrame, self)
288 @classmethod
289 def deserialize(cls, serialized: SerializableFrame) -> Self:
290 """Convert a serialized frame to an in-memory one."""
291 return cast(Self, serialized)
293 @property
294 def _ast_ident(self) -> str:
295 return "GENERAL"
298class SkyFrame(enum.StrEnum):
299 """The special frame that represents the sky, in ICRS coordinates."""
301 ICRS = "ICRS"
303 @property
304 def unit(self) -> u.UnitBase:
305 """Units of the coordinates in this frame
306 (`astropy.units.UnitBase`).
307 """
308 return u.rad
310 def standardize_x[T: float | np.ndarray](self, x: T) -> T:
311 """Coerce ``x`` coordinates into their standard range."""
312 return _wrap_positive(x)
314 def standardize_y[T: float | np.ndarray](self, y: T) -> T:
315 """Coerce ``x`` coordinates into their standard range."""
316 return _wrap_symmetric(y)
318 def serialize(self) -> SerializableFrame:
319 """Return a Pydantic-serializable version of this Frame."""
320 return cast(SerializableFrame, self)
322 @classmethod
323 def deserialize(cls, serialized: SerializableFrame) -> Self:
324 """Convert a serialized frame to an in-memory one."""
325 return cast(Self, serialized)
327 @property
328 def _ast_ident(self) -> str:
329 return self.value
332ICRS = SkyFrame.ICRS
335type SerializableFrame = (
336 SkyFrame
337 | Annotated[
338 DetectorFrame | TractFrame | FocalPlaneFrame | FieldAngleFrame | GeneralFrame,
339 pydantic.Field(discriminator="frame_type"),
340 ]
341)
344_TWOPI: float = np.pi * 2
347def _camera_ast_ident(instrument: str, visit: int | None) -> str:
348 return f"{instrument}@{visit}" if visit is not None else instrument
351def _wrap_positive[T: float | np.ndarray](a: T) -> T:
352 return a % _TWOPI # type: ignore[return-value]
355def _wrap_symmetric[T: float | np.ndarray](a: T) -> T:
356 return (a + np.pi) % _TWOPI - np.pi # type: ignore[return-value]