Coverage for python/astro_metadata_translator/translators/helpers.py: 18%
53 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-06 03:48 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-06 03:48 -0700
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 "to_location_via_telescope_name",
28 "is_non_science",
29 "tracking_from_degree_headers",
30 "altitude_from_zenith_distance",
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 Returns
52 -------
53 loc : `astropy.coordinates.EarthLocation`
54 Location of the observatory.
55 """
56 return EarthLocation.of_site(self.to_telescope())
59def is_non_science(self: MetadataTranslator) -> None:
60 """Raise an exception if this is a science observation.
62 Raises
63 ------
64 KeyError
65 Is a science observation.
66 """
67 if self.to_observation_type() == "science":
68 raise KeyError(f"{self._log_prefix}: Header represents science observation and can not default")
71def altitude_from_zenith_distance(zd: astropy.units.Quantity) -> astropy.units.Quantity:
72 """Convert zenith distance to altitude.
74 Parameters
75 ----------
76 zd : `astropy.units.Quantity`
77 Zenith distance as an angle.
79 Returns
80 -------
81 alt : `astropy.units.Quantity`
82 Altitude.
83 """
84 return 90.0 * u.deg - zd
87def tracking_from_degree_headers(
88 self: MetadataTranslator,
89 radecsys: Sequence[str],
90 radecpairs: tuple[tuple[str, str], ...],
91 unit: astropy.units.Unit = u.deg,
92) -> SkyCoord:
93 """Calculate the tracking coordinates from lists of headers.
95 Parameters
96 ----------
97 radecsys : `list` or `tuple`
98 Header keywords to try corresponding to the tracking system. If none
99 match ICRS will be assumed.
100 radecpairs : `tuple` of `tuple` of pairs of `str`
101 Pairs of keywords specifying the RA/Dec in units of ``unit``.
102 unit : `astropy.unit.BaseUnit` or `tuple`
103 Unit definition suitable for the `~astropy.coordinate.SkyCoord`
104 constructor.
106 Returns
107 -------
108 radec = `astropy.coordinates.SkyCoord`
109 The RA/Dec coordinates. None if this is a moving target or a
110 non-science observation without any RA/Dec definition.
112 Raises
113 ------
114 KeyError
115 No RA/Dec keywords were found and this observation is a science
116 observation.
117 """
118 used = []
119 for k in radecsys:
120 if self.is_key_ok(k):
121 frame = self._header[k].strip().lower()
122 used.append(k)
123 if frame == "gappt":
124 self._used_these_cards(*used)
125 # Moving target
126 return None
127 break
128 else:
129 frame = "icrs"
130 for ra_key, dec_key in radecpairs:
131 if self.are_keys_ok([ra_key, dec_key]):
132 radec = SkyCoord(
133 self._header[ra_key],
134 self._header[dec_key],
135 frame=frame,
136 unit=unit,
137 obstime=self.to_datetime_begin(),
138 location=self.to_location(),
139 )
140 self._used_these_cards(ra_key, dec_key, *used)
141 return radec
142 if self.to_observation_type() == "science":
143 raise KeyError(f"{self._log_prefix}: Unable to determine tracking RA/Dec of science observation")
144 return None
147def altaz_from_degree_headers(
148 self: MetadataTranslator,
149 altazpairs: tuple[tuple[str, str], ...],
150 obstime: astropy.time.Time,
151 is_zd: set[str] | None = None,
152) -> AltAz:
153 """Calculate the altitude/azimuth coordinates from lists of headers.
155 If the altitude is found but is greater than 90 deg, it will be returned
156 fixed at 90 deg.
157 If the altitude or azimuth are negative and this is a calibration
158 observation, `None` will be returned.
160 Parameters
161 ----------
162 altazpairs : `tuple` of `str`
163 Pairs of keywords specifying Alt/Az in degrees. Each pair is tried
164 in turn.
165 obstime : `astropy.time.Time`
166 Reference time to use for these coordinates.
167 is_zd : `set`, optional
168 Contains keywords that correspond to zenith distances rather than
169 altitude.
171 Returns
172 -------
173 altaz = `astropy.coordinates.AltAz`
174 The AltAz coordinates associated with the telescope location
175 and provided time. Returns `None` if this observation is not
176 a science observation and no AltAz keys were located.
178 Raises
179 ------
180 KeyError
181 No AltAz keywords were found and this observation is a science
182 observation.
183 """
184 for alt_key, az_key in altazpairs:
185 if self.are_keys_ok([az_key, alt_key]):
186 az = self._header[az_key]
187 alt = self._header[alt_key]
189 # Check for zenith distance
190 if is_zd and alt_key in is_zd:
191 alt = altitude_from_zenith_distance(alt * u.deg).value
193 if az < -360.0 or alt < 0.0:
194 # Break out of loop since we have found values but
195 # they are bad.
196 break
197 if alt > 90.0:
198 log.warning("%s: Clipping altitude (%f) at 90 degrees", self._log_prefix, alt)
199 alt = 90.0
201 altaz = AltAz(az * u.deg, alt * u.deg, obstime=obstime, location=self.to_location())
202 self._used_these_cards(az_key, alt_key)
203 return altaz
204 if self.to_observation_type() == "science":
205 raise KeyError(f"{self._log_prefix}: Unable to determine AltAz of science observation")
206 return None