Coverage for python/astro_metadata_translator/translators/helpers.py: 15%

57 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-10-09 03:26 -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. 

11 

12"""Generically useful translation helpers which translation classes 

13can use. 

14 

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. 

19 

20Other functions are pure helpers that can be imported and used to help 

21translation classes without using `MetadataTranslator` properties. 

22""" 

23 

24from __future__ import annotations 

25 

26__all__ = ( 

27 "altitude_from_zenith_distance", 

28 "is_non_science", 

29 "to_location_via_telescope_name", 

30 "tracking_from_degree_headers", 

31) 

32 

33import logging 

34from collections.abc import Sequence 

35from typing import TYPE_CHECKING 

36 

37import astropy.units as u 

38from astropy.coordinates import AltAz, EarthLocation, SkyCoord 

39 

40if TYPE_CHECKING: 

41 import astropy.units 

42 

43 from ..translator import MetadataTranslator 

44 

45log = logging.getLogger(__name__) 

46 

47 

48def to_location_via_telescope_name(self: MetadataTranslator) -> EarthLocation: 

49 """Calculate the observatory location via the telescope name. 

50 

51 Returns 

52 ------- 

53 loc : `astropy.coordinates.EarthLocation` 

54 Location of the observatory. 

55 """ 

56 return EarthLocation.of_site(self.to_telescope()) 

57 

58 

59def is_non_science(self: MetadataTranslator) -> None: 

60 """Raise an exception if this is a science observation. 

61 

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") 

69 

70 

71def altitude_from_zenith_distance(zd: astropy.units.Quantity) -> astropy.units.Quantity: 

72 """Convert zenith distance to altitude. 

73 

74 Parameters 

75 ---------- 

76 zd : `astropy.units.Quantity` 

77 Zenith distance as an angle. 

78 

79 Returns 

80 ------- 

81 alt : `astropy.units.Quantity` 

82 Altitude. 

83 """ 

84 return 90.0 * u.deg - zd 

85 

86 

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. 

94 

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. 

105 

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. 

111 

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 

145 

146 

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 max_alt: float = 90.0, 

153 min_alt: float = 0.0, 

154) -> AltAz: 

155 """Calculate the altitude/azimuth coordinates from lists of headers. 

156 

157 If the altitude is found but is greater than the maximum allowed value, 

158 it will be returned fixed at that maximum value. 

159 If the altitude is found but is less than the minimum allowed value, it 

160 will be returned clipped to that minimum value. 

161 If the azimuth is less than -360.0 and this is not a science 

162 observation, `None` will be returned. 

163 

164 Parameters 

165 ---------- 

166 altazpairs : `tuple` of `str` 

167 Pairs of keywords specifying Alt/Az in degrees. Each pair is tried 

168 in turn. 

169 obstime : `astropy.time.Time` 

170 Reference time to use for these coordinates. 

171 is_zd : `set`, optional 

172 Contains keywords that correspond to zenith distances rather than 

173 altitude. 

174 max_alt : `float`, optional 

175 Maximum allowed altitude in degrees. Will be clamped to this value if 

176 out of range. This value will be forced to +90 if it exceeds +90 

177 since `astropy.coordinates.AltAz` does not allow a value larger than 

178 this even if that is caused by a telescope that can lean back at 

179 Zenith. 

180 min_alt : `float`, optional 

181 Minimum allowed altitude in degrees. Will be clamped to this value if 

182 out of range. 

183 

184 Returns 

185 ------- 

186 altaz = `astropy.coordinates.AltAz` 

187 The AltAz coordinates associated with the telescope location 

188 and provided time. Returns `None` if this observation is not 

189 a science observation and no AltAz keys were located. 

190 

191 Raises 

192 ------ 

193 KeyError 

194 No AltAz keywords were found and this observation is a science 

195 observation. 

196 """ 

197 if max_alt > 90.0: 

198 max_alt = 90.0 

199 for alt_key, az_key in altazpairs: 

200 if self.are_keys_ok([az_key, alt_key]): 

201 az = self._header[az_key] 

202 alt = self._header[alt_key] 

203 

204 # Check for zenith distance 

205 if is_zd and alt_key in is_zd: 

206 alt = altitude_from_zenith_distance(alt * u.deg).value 

207 

208 if az < -360.0 or alt < -90.0: 

209 # Break out of loop since we have found values but 

210 # they are not believable 

211 break 

212 if alt > max_alt: 

213 log.info("%s: Clipping altitude (%f) at %f degrees", self._log_prefix, alt, max_alt) 

214 alt = max_alt 

215 elif alt < min_alt: 

216 log.info("%s: Clipping altitude (%f) at %f degrees", self._log_prefix, alt, min_alt) 

217 alt = min_alt 

218 

219 altaz = AltAz(az * u.deg, alt * u.deg, obstime=obstime, location=self.to_location()) 

220 self._used_these_cards(az_key, alt_key) 

221 return altaz 

222 if self.to_observation_type() == "science": 

223 raise KeyError(f"{self._log_prefix}: Unable to determine AltAz of science observation") 

224 return None