Coverage for python / astro_metadata_translator / translators / helpers.py: 15%
57 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-13 01:21 -0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-13 01:21 -0800
1# This file is part of astro_metadata_translator.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the LICENSE 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.
12"""Generically useful translation helpers which translation classes
13can use.
15They are written as free functions. Some of them are written
16as if they are methods of `MetadataTranslator`, allowing them to be attached
17to translator classes that need them. These methods have full access to
18the translator methods.
20Other functions are pure helpers that can be imported and used to help
21translation classes without using `MetadataTranslator` properties.
22"""
24from __future__ import annotations
26__all__ = (
27 "altitude_from_zenith_distance",
28 "is_non_science",
29 "to_location_via_telescope_name",
30 "tracking_from_degree_headers",
31)
33import logging
34from collections.abc import Sequence
35from typing import TYPE_CHECKING
37import astropy.units as u
38from astropy.coordinates import AltAz, EarthLocation, SkyCoord
40if TYPE_CHECKING:
41 import astropy.units
43 from ..translator import MetadataTranslator
45log = logging.getLogger(__name__)
48def to_location_via_telescope_name(self: MetadataTranslator) -> EarthLocation:
49 """Calculate the observatory location via the telescope name.
51 Parameters
52 ----------
53 self : `MetadataTranslator`
54 The translator being used.
56 Returns
57 -------
58 loc : `astropy.coordinates.EarthLocation`
59 Location of the observatory.
60 """
61 return EarthLocation.of_site(self.to_telescope())
64def is_non_science(self: MetadataTranslator) -> None:
65 """Raise an exception if this is a science observation.
67 Parameters
68 ----------
69 self : `MetadataTranslator`
70 The translator being used.
72 Raises
73 ------
74 KeyError
75 Is a science observation.
76 """
77 if self.to_observation_type() == "science":
78 raise KeyError(f"{self._log_prefix}: Header represents science observation and can not default")
81def altitude_from_zenith_distance(zd: astropy.units.Quantity) -> astropy.units.Quantity:
82 """Convert zenith distance to altitude.
84 Parameters
85 ----------
86 zd : `astropy.units.Quantity`
87 Zenith distance as an angle.
89 Returns
90 -------
91 alt : `astropy.units.Quantity`
92 Altitude.
93 """
94 return 90.0 * u.deg - zd
97def tracking_from_degree_headers(
98 self: MetadataTranslator,
99 radecsys: Sequence[str],
100 radecpairs: tuple[tuple[str, str], ...],
101 unit: astropy.units.Unit = u.deg,
102) -> SkyCoord:
103 """Calculate the tracking coordinates from lists of headers.
105 Parameters
106 ----------
107 self : `MetadataTranslator`
108 The translator being used.
109 radecsys : `list` or `tuple`
110 Header keywords to try corresponding to the tracking system. If none
111 match ICRS will be assumed.
112 radecpairs : `tuple` of `tuple` of pairs of `str`
113 Pairs of keywords specifying the RA/Dec in units of ``unit``.
114 unit : `astropy.unit.BaseUnit` or `tuple`
115 Unit definition suitable for the `~astropy.coordinate.SkyCoord`
116 constructor.
118 Returns
119 -------
120 radec = `astropy.coordinates.SkyCoord`
121 The RA/Dec coordinates. None if this is a moving target or a
122 non-science observation without any RA/Dec definition.
124 Raises
125 ------
126 KeyError
127 No RA/Dec keywords were found and this observation is a science
128 observation.
129 """
130 used = []
131 for k in radecsys:
132 if self.is_key_ok(k):
133 frame = self._header[k].strip().lower()
134 used.append(k)
135 if frame == "gappt":
136 self._used_these_cards(*used)
137 # Moving target
138 return None
139 break
140 else:
141 frame = "icrs"
142 for ra_key, dec_key in radecpairs:
143 if self.are_keys_ok([ra_key, dec_key]):
144 radec = SkyCoord(
145 self._header[ra_key],
146 self._header[dec_key],
147 frame=frame,
148 unit=unit,
149 obstime=self.to_datetime_begin(),
150 location=self.to_location(),
151 )
152 self._used_these_cards(ra_key, dec_key, *used)
153 return radec
154 if self.to_observation_type() == "science":
155 raise KeyError(f"{self._log_prefix}: Unable to determine tracking RA/Dec of science observation")
156 return None
159def altaz_from_degree_headers(
160 self: MetadataTranslator,
161 altazpairs: tuple[tuple[str, str], ...],
162 obstime: astropy.time.Time,
163 is_zd: set[str] | None = None,
164 max_alt: float = 90.0,
165 min_alt: float = 0.0,
166) -> AltAz:
167 """Calculate the altitude/azimuth coordinates from lists of headers.
169 If the altitude is found but is greater than the maximum allowed value,
170 it will be returned fixed at that maximum value.
171 If the altitude is found but is less than the minimum allowed value, it
172 will be returned clipped to that minimum value.
173 If the azimuth is less than -360.0 and this is not a science
174 observation, `None` will be returned.
176 Parameters
177 ----------
178 self : `MetadataTranslator`
179 The translator being used.
180 altazpairs : `tuple` of `str`
181 Pairs of keywords specifying Alt/Az in degrees. Each pair is tried
182 in turn.
183 obstime : `astropy.time.Time`
184 Reference time to use for these coordinates.
185 is_zd : `set`, optional
186 Contains keywords that correspond to zenith distances rather than
187 altitude.
188 max_alt : `float`, optional
189 Maximum allowed altitude in degrees. Will be clamped to this value if
190 out of range. This value will be forced to +90 if it exceeds +90
191 since `astropy.coordinates.AltAz` does not allow a value larger than
192 this even if that is caused by a telescope that can lean back at
193 Zenith.
194 min_alt : `float`, optional
195 Minimum allowed altitude in degrees. Will be clamped to this value if
196 out of range.
198 Returns
199 -------
200 altaz = `astropy.coordinates.AltAz`
201 The AltAz coordinates associated with the telescope location
202 and provided time. Returns `None` if this observation is not
203 a science observation and no AltAz keys were located.
205 Raises
206 ------
207 KeyError
208 No AltAz keywords were found and this observation is a science
209 observation.
210 """
211 if max_alt > 90.0:
212 max_alt = 90.0
213 for alt_key, az_key in altazpairs:
214 if self.are_keys_ok([az_key, alt_key]):
215 az = self._header[az_key]
216 alt = self._header[alt_key]
218 # Check for zenith distance
219 if is_zd and alt_key in is_zd:
220 alt = altitude_from_zenith_distance(alt * u.deg).value
222 if az < -360.0 or alt < -90.0:
223 # Break out of loop since we have found values but
224 # they are not believable
225 break
226 if alt > max_alt:
227 log.info("%s: Clipping altitude (%f) at %f degrees", self._log_prefix, alt, max_alt)
228 alt = max_alt
229 elif alt < min_alt:
230 log.info("%s: Clipping altitude (%f) at %f degrees", self._log_prefix, alt, min_alt)
231 alt = min_alt
233 altaz = AltAz(az * u.deg, alt * u.deg, obstime=obstime, location=self.to_location())
234 self._used_these_cards(az_key, alt_key)
235 return altaz
236 if self.to_observation_type() == "science":
237 raise KeyError(f"{self._log_prefix}: Unable to determine AltAz of science observation")
238 return None