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

57 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-10-25 15:15 +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""" 

24 

25from __future__ import annotations 

26 

27__all__ = ( 

28 "to_location_via_telescope_name", 

29 "is_non_science", 

30 "tracking_from_degree_headers", 

31 "altitude_from_zenith_distance", 

32) 

33 

34import logging 

35from collections.abc import Sequence 

36from typing import TYPE_CHECKING 

37 

38import astropy.units as u 

39from astropy.coordinates import AltAz, EarthLocation, SkyCoord 

40 

41if TYPE_CHECKING: 41 ↛ 42line 41 didn't jump to line 42, because the condition on line 41 was never true

42 import astropy.units 

43 

44 from ..translator import MetadataTranslator 

45 

46log = logging.getLogger(__name__) 

47 

48 

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

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

51 

52 Returns 

53 ------- 

54 loc : `astropy.coordinates.EarthLocation` 

55 Location of the observatory. 

56 """ 

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

58 

59 

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

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

62 

63 Raises 

64 ------ 

65 KeyError 

66 Is a science observation. 

67 """ 

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

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

70 return 

71 

72 

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

74 """Convert zenith distance to altitude. 

75 

76 Parameters 

77 ---------- 

78 zd : `astropy.units.Quantity` 

79 Zenith distance as an angle. 

80 

81 Returns 

82 ------- 

83 alt : `astropy.units.Quantity` 

84 Altitude. 

85 """ 

86 return 90.0 * u.deg - zd 

87 

88 

89def tracking_from_degree_headers( 

90 self: MetadataTranslator, 

91 radecsys: Sequence[str], 

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

93 unit: astropy.units.Unit = u.deg, 

94) -> SkyCoord: 

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

96 

97 Parameters 

98 ---------- 

99 radecsys : `list` or `tuple` 

100 Header keywords to try corresponding to the tracking system. If none 

101 match ICRS will be assumed. 

102 radecpairs : `tuple` of `tuple` of pairs of `str` 

103 Pairs of keywords specifying the RA/Dec in units of ``unit``. 

104 unit : `astropy.unit.BaseUnit` or `tuple` 

105 Unit definition suitable for the `~astropy.coordinate.SkyCoord` 

106 constructor. 

107 

108 Returns 

109 ------- 

110 radec = `astropy.coordinates.SkyCoord` 

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

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

113 

114 Raises 

115 ------ 

116 KeyError 

117 No RA/Dec keywords were found and this observation is a science 

118 observation. 

119 """ 

120 used = [] 

121 for k in radecsys: 

122 if self.is_key_ok(k): 

123 frame = self._header[k].strip().lower() 

124 used.append(k) 

125 if frame == "gappt": 

126 self._used_these_cards(*used) 

127 # Moving target 

128 return None 

129 break 

130 else: 

131 frame = "icrs" 

132 for ra_key, dec_key in radecpairs: 

133 if self.are_keys_ok([ra_key, dec_key]): 

134 radec = SkyCoord( 

135 self._header[ra_key], 

136 self._header[dec_key], 

137 frame=frame, 

138 unit=unit, 

139 obstime=self.to_datetime_begin(), 

140 location=self.to_location(), 

141 ) 

142 self._used_these_cards(ra_key, dec_key, *used) 

143 return radec 

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

145 raise KeyError(f"{self._log_prefix}: Unable to determine tracking RA/Dec of science observation") 

146 return None 

147 

148 

149def altaz_from_degree_headers( 

150 self: MetadataTranslator, 

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

152 obstime: astropy.time.Time, 

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

154) -> AltAz: 

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

156 

157 If the altitude is found but is greater than 90 deg, it will be returned 

158 fixed at 90 deg. 

159 If the altitude or azimuth are negative and this is a calibration 

160 observation, `None` will be returned. 

161 

162 Parameters 

163 ---------- 

164 altazpairs : `tuple` of `str` 

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

166 in turn. 

167 obstime : `astropy.time.Time` 

168 Reference time to use for these coordinates. 

169 is_zd : `set`, optional 

170 Contains keywords that correspond to zenith distances rather than 

171 altitude. 

172 

173 Returns 

174 ------- 

175 altaz = `astropy.coordinates.AltAz` 

176 The AltAz coordinates associated with the telescope location 

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

178 a science observation and no AltAz keys were located. 

179 

180 Raises 

181 ------ 

182 KeyError 

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

184 observation. 

185 """ 

186 for alt_key, az_key in altazpairs: 

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

188 az = self._header[az_key] 

189 alt = self._header[alt_key] 

190 

191 # Check for zenith distance 

192 if is_zd and alt_key in is_zd: 

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

194 

195 if az < -360.0 or alt < 0.0: 

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

197 # they are bad. 

198 break 

199 if alt > 90.0: 

200 log.warning("%s: Clipping altitude (%f) at 90 degrees", self._log_prefix, alt) 

201 alt = 90.0 

202 

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

204 self._used_these_cards(az_key, alt_key) 

205 return altaz 

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

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

208 return None