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

57 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-14 23:38 +0000

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(translator: MetadataTranslator) -> EarthLocation: 

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

50 

51 Parameters 

52 ---------- 

53 translator : `MetadataTranslator` 

54 The translator being used. 

55 

56 Returns 

57 ------- 

58 loc : `astropy.coordinates.EarthLocation` 

59 Location of the observatory. 

60 """ 

61 return EarthLocation.of_site(translator.to_telescope()) 

62 

63 

64def is_non_science(translator: MetadataTranslator) -> None: 

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

66 

67 Parameters 

68 ---------- 

69 translator : `MetadataTranslator` 

70 The translator being used. 

71 

72 Raises 

73 ------ 

74 KeyError 

75 Is a science observation. 

76 """ 

77 if translator.to_observation_type() == "science": 

78 raise KeyError(f"{translator._log_prefix}: Header represents science observation and can not default") 

79 

80 

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

82 """Convert zenith distance to altitude. 

83 

84 Parameters 

85 ---------- 

86 zd : `astropy.units.Quantity` 

87 Zenith distance as an angle. 

88 

89 Returns 

90 ------- 

91 alt : `astropy.units.Quantity` 

92 Altitude. 

93 """ 

94 return 90.0 * u.deg - zd 

95 

96 

97def tracking_from_degree_headers( 

98 translator: MetadataTranslator, 

99 radecsys: Sequence[str], 

100 radecpairs: tuple[tuple[str, str], ...], 

101 unit: astropy.units.Unit | tuple[astropy.units.Unit, ...] = u.deg, 

102) -> SkyCoord | None: 

103 """Calculate the tracking coordinates from lists of headers. 

104 

105 Parameters 

106 ---------- 

107 translator : `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. 

117 

118 Returns 

119 ------- 

120 radec : `astropy.coordinates.SkyCoord` or `None` 

121 The RA/Dec coordinates. `None` if this is a moving target or a 

122 non-science observation without any RA/Dec definition. 

123 

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 translator.is_key_ok(k): 

133 frame = translator._header[k].strip().lower() 

134 used.append(k) 

135 if frame == "gappt": 

136 translator._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 translator.are_keys_ok([ra_key, dec_key]): 

144 radec = SkyCoord( 

145 translator._header[ra_key], 

146 translator._header[dec_key], 

147 frame=frame, 

148 unit=unit, 

149 obstime=translator.to_datetime_begin(), 

150 location=translator.to_location(), 

151 ) 

152 translator._used_these_cards(ra_key, dec_key, *used) 

153 return radec 

154 if translator.to_observation_type() == "science": 

155 raise KeyError( 

156 f"{translator._log_prefix}: Unable to determine tracking RA/Dec of science observation" 

157 ) 

158 return None 

159 

160 

161def altaz_from_degree_headers( 

162 translator: MetadataTranslator, 

163 altazpairs: tuple[tuple[str, str], ...], 

164 obstime: astropy.time.Time, 

165 is_zd: set[str] | None = None, 

166 max_alt: float = 90.0, 

167 min_alt: float = 0.0, 

168) -> AltAz | None: 

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

170 

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

172 it will be returned fixed at that maximum value. 

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

174 will be returned clipped to that minimum value. 

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

176 observation, `None` will be returned. 

177 

178 Parameters 

179 ---------- 

180 translator : `MetadataTranslator` 

181 The translator being used. 

182 altazpairs : `tuple` of `str` 

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

184 in turn. 

185 obstime : `astropy.time.Time` 

186 Reference time to use for these coordinates. 

187 is_zd : `set`, optional 

188 Contains keywords that correspond to zenith distances rather than 

189 altitude. 

190 max_alt : `float`, optional 

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

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

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

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

195 Zenith. 

196 min_alt : `float`, optional 

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

198 out of range. 

199 

200 Returns 

201 ------- 

202 altaz : `astropy.coordinates.AltAz` or `None` 

203 The AltAz coordinates associated with the telescope location 

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

205 a science observation and no AltAz keys were located. 

206 

207 Raises 

208 ------ 

209 KeyError 

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

211 observation. 

212 """ 

213 if max_alt > 90.0: 

214 max_alt = 90.0 

215 for alt_key, az_key in altazpairs: 

216 if translator.are_keys_ok([az_key, alt_key]): 

217 az = translator._header[az_key] 

218 alt = translator._header[alt_key] 

219 

220 # Check for zenith distance 

221 if is_zd and alt_key in is_zd: 

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

223 

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

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

226 # they are not believable 

227 break 

228 if alt > max_alt: 

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

230 alt = max_alt 

231 elif alt < min_alt: 

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

233 alt = min_alt 

234 

235 altaz = AltAz(az * u.deg, alt * u.deg, obstime=obstime, location=translator.to_location()) 

236 translator._used_these_cards(az_key, alt_key) 

237 return altaz 

238 if translator.to_observation_type() == "science": 

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

240 return None