Coverage for python / lsst / images / _transforms / _ast.py: 12%
217 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
14from collections.abc import Iterable
15from typing import Any
17__all__ = (
18 "USING_STARLINK_PYAST",
19 "FitsChan",
20 "Frame",
21 "FrameSet",
22 "Mapping",
23 "ShiftMap",
24 "SkyFrame",
25 "StringStream",
26 "UnitMap",
27 "wrap_frame_set",
28 "wrap_mapping",
29)
31try:
32 import starlink.Ast as StarAst
33except ImportError:
34 StarAst = None
35 import astshim as AstShim
37 USING_STARLINK_PYAST = False
38else:
39 AstShim = None
40 USING_STARLINK_PYAST = True
42# Public backend-dependent bindings.
43StringStream: Any
44FitsChan: Any
45Mapping: Any
46UnitMap: Any
47ShiftMap: Any
48Frame: Any
49SkyFrame: Any
50FrameSet: Any
53if USING_STARLINK_PYAST: 53 ↛ 54line 53 didn't jump to line 54 because the condition on line 53 was never true
54 assert StarAst is not None
56 class _StringStream:
57 """A source/sink object compatible with starlink.Ast channel APIs."""
59 def __init__(self, text: str = ""):
60 if "\n" in text or "\r" in text:
61 self._lines = text.splitlines()
62 elif text and len(text) % 80 == 0:
63 # Astropy WCS.to_header_string() yields a single concatenated
64 # FITS header block; FitsChan expects one card per source line.
65 self._lines = [text[n : n + 80] for n in range(0, len(text), 80)]
66 else:
67 self._lines = text.splitlines()
69 def astsource(self) -> str | None:
70 if not self._lines:
71 return None
72 return self._lines.pop(0)
74 def astsink(self, line: str) -> None:
75 self._lines.append(line)
77 def to_string(self) -> str:
78 if not self._lines:
79 return ""
80 return "\n".join(self._lines) + "\n"
82 class _AstWrapper:
83 def __init__(self, obj: Any):
84 self._obj = obj
86 @classmethod
87 def _from_obj(cls, obj: Any) -> Any:
88 self = cls.__new__(cls)
89 _AstWrapper.__init__(self, obj)
90 return self
92 def __getattr__(self, name: str) -> Any:
93 return _wrap_ast_object(getattr(self._obj, name))
95 def _serialize_ast_object(obj: Any, showComments: bool = True) -> str:
96 sink = _StringStream()
97 options = "" if showComments else "Comment=0"
98 chan = StarAst.Channel(None, sink, options=options)
99 chan.write(_unwrap_ast_object(obj))
100 return sink.to_string()
102 def _deserialize_ast_object(serialized: str) -> Any:
103 source = _StringStream(serialized)
104 chan = StarAst.Channel(source)
105 return chan.read()
107 def _coerce_to_star_ast_object(obj: Any) -> Any:
108 match obj:
109 case _AstWrapper():
110 return obj._obj
111 case StarAst.FrameSet() | StarAst.Mapping() | StarAst.Frame():
112 return obj
113 case _:
114 if hasattr(obj, "show"):
115 try:
116 return _deserialize_ast_object(obj.show())
117 except Exception:
118 return obj
119 return obj
121 def _unwrap_ast_object(obj: Any) -> Any:
122 return _coerce_to_star_ast_object(obj)
124 def _wrap_ast_object(obj: Any) -> Any:
125 match obj:
126 case None:
127 return obj
128 case _AstWrapper():
129 return obj
130 case StarAst.FrameSet():
131 return _FrameSet._from_obj(obj)
132 case StarAst.SkyFrame():
133 return _SkyFrame._from_obj(obj)
134 case StarAst.Frame():
135 return _Frame._from_obj(obj)
136 case StarAst.Mapping():
137 return _Mapping._from_obj(obj)
138 case _:
139 coerced = _coerce_to_star_ast_object(obj)
140 if coerced is not obj:
141 return _wrap_ast_object(coerced)
142 return obj
144 class _Mapping(_AstWrapper):
145 @staticmethod
146 def fromString(serialized: str) -> _Mapping:
147 obj = _deserialize_ast_object(serialized)
148 if not isinstance(obj, StarAst.Mapping):
149 raise TypeError(f"Serialized object is not a Mapping: {type(obj)}")
150 return _Mapping._from_obj(obj)
152 def show(self, showComments: bool = True) -> str:
153 return _serialize_ast_object(self._obj, showComments=showComments)
155 def simplified(self) -> _Mapping:
156 return _Mapping._from_obj(self._obj.simplify())
158 def applyForward(self, xy: Any) -> Any:
159 return self._obj.tran(xy, True)
161 def applyInverse(self, xy: Any) -> Any:
162 return self._obj.tran(xy, False)
164 def then(self, other: _Mapping) -> _Mapping:
165 return _Mapping._from_obj(StarAst.CmpMap(self._obj, _unwrap_ast_object(other), True))
167 def inverted(self) -> _Mapping:
168 copy = self._obj.copy()
169 copy.invert()
170 return _Mapping._from_obj(copy)
172 class _UnitMap(_Mapping):
173 def __init__(self, n_coord: int):
174 super().__init__(StarAst.UnitMap(n_coord))
176 class _ShiftMap(_Mapping):
177 def __init__(self, shift: Iterable[float]):
178 super().__init__(StarAst.ShiftMap(list(shift)))
180 class _Frame(_AstWrapper):
181 def __init__(self, n_axes: int, options: str = ""):
182 super().__init__(StarAst.Frame(n_axes, options))
184 @property
185 def ident(self) -> str:
186 return self._obj.Ident
188 def setUnit(self, axis: int, unit: str) -> None:
189 setattr(self._obj, f"Unit_{axis}", unit)
191 def getUnit(self, axis: int) -> str:
192 return getattr(self._obj, f"Unit_{axis}")
194 def setLabel(self, axis: int, label: str) -> None:
195 setattr(self._obj, f"Label_{axis}", label)
197 def getBottom(self, axis: int) -> float:
198 return getattr(self._obj, f"Bottom_{axis}")
200 def getTop(self, axis: int) -> float:
201 return getattr(self._obj, f"Top_{axis}")
203 def show(self) -> str:
204 return _serialize_ast_object(self._obj)
206 class _SkyFrame(_Frame):
207 def __init__(self, options: str = ""):
208 _AstWrapper.__init__(self, StarAst.SkyFrame(options))
210 class _FrameSet(_AstWrapper):
211 BASE = 1
213 def __init__(self, base_frame: _Frame):
214 super().__init__(StarAst.FrameSet(_unwrap_ast_object(base_frame)))
216 @property
217 def nFrame(self) -> int:
218 return self._obj.Nframe
220 @property
221 def base(self) -> int:
222 return self._obj.Base
224 @base.setter
225 def base(self, value: int) -> None:
226 self._obj.Base = value
228 @property
229 def current(self) -> int:
230 return self._obj.Current
232 @current.setter
233 def current(self, value: int) -> None:
234 self._obj.Current = value
236 def addFrame(self, iframe: int, mapping: _Mapping, frame: _Frame) -> None:
237 self._obj.addframe(iframe, _unwrap_ast_object(mapping), _unwrap_ast_object(frame))
239 def getFrame(self, iframe: int, copy: bool = True) -> _Frame:
240 del copy
241 return _wrap_ast_object(self._obj.getframe(iframe))
243 def getMapping(self, iframe1: int | None = None, iframe2: int | None = None) -> _Mapping:
244 if iframe1 is None:
245 iframe1 = self.base
246 if iframe2 is None:
247 iframe2 = self.current
248 return _wrap_ast_object(self._obj.getmapping(iframe1, iframe2))
250 def show(self) -> str:
251 return _serialize_ast_object(self._obj)
253 @staticmethod
254 def fromString(serialized: str) -> _FrameSet:
255 obj = _deserialize_ast_object(serialized)
256 if not isinstance(obj, StarAst.FrameSet):
257 raise TypeError(f"Serialized object is not a FrameSet: {type(obj)}")
258 return _FrameSet._from_obj(obj)
260 class _FitsChan:
261 def __init__(self, stream: _StringStream | None = None, options: str = ""):
262 source = stream if stream is not None else None
263 sink = stream if stream is not None else None
264 self._obj = StarAst.FitsChan(source, sink, options)
266 def read(self) -> Any:
267 return _wrap_ast_object(self._obj.read())
269 def write(self, obj: Any) -> int:
270 return self._obj.write(_unwrap_ast_object(obj))
272 def setFitsI(self, keyword: str, value: int) -> None:
273 self._obj.setfitsI(keyword, value, "", 1)
275 def __iter__(self) -> Any:
276 return iter(self._obj)
278 StringStream = _StringStream
279 FitsChan = _FitsChan
280 Mapping = _Mapping
281 UnitMap = _UnitMap
282 ShiftMap = _ShiftMap
283 Frame = _Frame
284 SkyFrame = _SkyFrame
285 FrameSet = _FrameSet
287 def wrap_mapping(mapping: Any) -> Any:
288 return _wrap_ast_object(mapping)
290 def wrap_frame_set(frame_set: Any) -> Any:
291 return _wrap_ast_object(frame_set)
293else:
294 assert AstShim is not None
296 StringStream = AstShim.StringStream
297 FitsChan = AstShim.FitsChan
298 Mapping = AstShim.Mapping
299 UnitMap = AstShim.UnitMap
300 ShiftMap = AstShim.ShiftMap
301 Frame = AstShim.Frame
302 SkyFrame = AstShim.SkyFrame
303 FrameSet = AstShim.FrameSet
305 def wrap_mapping(mapping: Any) -> Any:
306 return mapping
308 def wrap_frame_set(frame_set: Any) -> Any:
309 return frame_set