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

56 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-01-10 02:29 -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. 

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 typing import TYPE_CHECKING, Optional, Sequence, Set, Tuple 

36 

37import astropy.units as u 

38from astropy.coordinates import AltAz, EarthLocation, SkyCoord 

39 

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

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 return 

70 

71 

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

73 """Convert zenith distance to altitude 

74 

75 Parameters 

76 ---------- 

77 zd : `astropy.units.Quantity` 

78 Zenith distance as an angle. 

79 

80 Returns 

81 ------- 

82 alt : `astropy.units.Quantity` 

83 Altitude. 

84 """ 

85 return 90.0 * u.deg - zd 

86 

87 

88def tracking_from_degree_headers( 

89 self: MetadataTranslator, 

90 radecsys: Sequence[str], 

91 radecpairs: Tuple[Tuple[str, str], ...], 

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

93) -> SkyCoord: 

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

95 

96 Parameters 

97 ---------- 

98 radecsys : `list` or `tuple` 

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

100 match ICRS will be assumed. 

101 radecpairs : `tuple` of `tuple` of pairs of `str` 

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

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

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

105 constructor. 

106 

107 Returns 

108 ------- 

109 radec = `astropy.coordinates.SkyCoord` 

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

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

112 

113 Raises 

114 ------ 

115 KeyError 

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

117 observation. 

118 """ 

119 used = [] 

120 for k in radecsys: 

121 if self.is_key_ok(k): 

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

123 used.append(k) 

124 if frame == "gappt": 

125 self._used_these_cards(*used) 

126 # Moving target 

127 return None 

128 break 

129 else: 

130 frame = "icrs" 

131 for ra_key, dec_key in radecpairs: 

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

133 radec = SkyCoord( 

134 self._header[ra_key], 

135 self._header[dec_key], 

136 frame=frame, 

137 unit=unit, 

138 obstime=self.to_datetime_begin(), 

139 location=self.to_location(), 

140 ) 

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

142 return radec 

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

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

145 return None 

146 

147 

148def altaz_from_degree_headers( 

149 self: MetadataTranslator, 

150 altazpairs: Tuple[Tuple[str, str], ...], 

151 obstime: astropy.time.Time, 

152 is_zd: Optional[Set[str]] = None, 

153) -> AltAz: 

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

155 

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

157 fixed at 90 deg. 

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

159 observation, `None` will be returned. 

160 

161 Parameters 

162 ---------- 

163 altazpairs : `tuple` of `str` 

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

165 in turn. 

166 obstime : `astropy.time.Time` 

167 Reference time to use for these coordinates. 

168 is_zd : `set`, optional 

169 Contains keywords that correspond to zenith distances rather than 

170 altitude. 

171 

172 Returns 

173 ------- 

174 altaz = `astropy.coordinates.AltAz` 

175 The AltAz coordinates associated with the telescope location 

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

177 a science observation and no AltAz keys were located. 

178 

179 Raises 

180 ------ 

181 KeyError 

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

183 observation. 

184 """ 

185 for alt_key, az_key in altazpairs: 

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

187 az = self._header[az_key] 

188 alt = self._header[alt_key] 

189 

190 # Check for zenith distance 

191 if is_zd and alt_key in is_zd: 

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

193 

194 if az < -360.0 or alt < 0.0: 

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

196 # they are bad. 

197 break 

198 if alt > 90.0: 

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

200 alt = 90.0 

201 

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

203 self._used_these_cards(az_key, alt_key) 

204 return altaz 

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

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

207 return None