Coverage for python/astro_metadata_translator/translators/fits.py: 31%

57 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-08-30 02:23 -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"""Metadata translation code for standard FITS headers""" 

13 

14from __future__ import annotations 

15 

16__all__ = ("FitsTranslator",) 

17 

18from typing import Any, Dict, List, MutableMapping, Optional, Tuple, Union 

19 

20import astropy.units as u 

21from astropy.coordinates import EarthLocation 

22from astropy.time import Time 

23 

24from ..translator import MetadataTranslator, cache_translation 

25 

26 

27class FitsTranslator(MetadataTranslator): 

28 """Metadata translator for FITS standard headers. 

29 

30 Understands: 

31 

32 - DATE-OBS 

33 - INSTRUME 

34 - TELESCOP 

35 - OBSGEO-[X,Y,Z] 

36 

37 """ 

38 

39 # Direct translation from header key to standard form 

40 _trivial_map: Dict[str, Union[str, List[str], Tuple[Any, ...]]] = dict( 

41 instrument="INSTRUME", 

42 telescope="TELESCOP", 

43 ) 

44 

45 @classmethod 

46 def can_translate(cls, header: MutableMapping[str, Any], filename: Optional[str] = None) -> bool: 

47 """Indicate whether this translation class can translate the 

48 supplied header. 

49 

50 Checks the instrument value and compares with the supported 

51 instruments in the class 

52 

53 Parameters 

54 ---------- 

55 header : `dict`-like 

56 Header to convert to standardized form. 

57 filename : `str`, optional 

58 Name of file being translated. 

59 

60 Returns 

61 ------- 

62 can : `bool` 

63 `True` if the header is recognized by this class. `False` 

64 otherwise. 

65 """ 

66 if cls.supported_instrument is None: 

67 return False 

68 

69 # Protect against being able to always find a standard 

70 # header for instrument 

71 try: 

72 translator = cls(header, filename=filename) 

73 instrument = translator.to_instrument() 

74 except KeyError: 

75 return False 

76 

77 return instrument == cls.supported_instrument 

78 

79 @classmethod 

80 def _from_fits_date_string( 

81 cls, date_str: str, scale: str = "utc", time_str: Optional[str] = None 

82 ) -> Time: 

83 """Parse standard FITS ISO-style date string and return time object 

84 

85 Parameters 

86 ---------- 

87 date_str : `str` 

88 FITS format date string to convert to standard form. Bypasses 

89 lookup in the header. 

90 scale : `str`, optional 

91 Override the time scale from the TIMESYS header. Defaults to 

92 UTC. 

93 time_str : `str`, optional 

94 If provided, overrides any time component in the ``dateStr``, 

95 retaining the YYYY-MM-DD component and appending this time 

96 string, assumed to be of format HH:MM::SS.ss. 

97 

98 Returns 

99 ------- 

100 date : `astropy.time.Time` 

101 `~astropy.time.Time` representation of the date. 

102 """ 

103 if time_str is not None: 

104 date_str = "{}T{}".format(date_str[:10], time_str) 

105 

106 return Time(date_str, format="isot", scale=scale) 

107 

108 def _from_fits_date( 

109 self, date_key: str, mjd_key: Optional[str] = None, scale: Optional[str] = None 

110 ) -> Time: 

111 """Calculate a date object from the named FITS header 

112 

113 Uses the TIMESYS header if present to determine the time scale, 

114 defaulting to UTC. Can be overridden since sometimes headers 

115 use TIMESYS for DATE- style headers but also have headers using 

116 different time scales. 

117 

118 Parameters 

119 ---------- 

120 date_key : `str` 

121 The key in the header representing a standard FITS 

122 ISO-style date. Can be `None` to go straight to MJD key. 

123 mjd_key : `str`, optional 

124 The key in the header representing a standard FITS MJD 

125 style date. This key will be tried if ``date_key`` is not 

126 found, is `None`, or can not be parsed. 

127 scale : `str`, optional 

128 Override value to use for the time scale in preference to 

129 TIMESYS or the default. Should be a form understood by 

130 `~astropy.time.Time`. 

131 

132 Returns 

133 ------- 

134 date : `astropy.time.Time` 

135 `~astropy.time.Time` representation of the date. 

136 """ 

137 used = [] 

138 if scale is not None: 

139 pass 

140 elif self.is_key_ok("TIMESYS"): 

141 scale = self._header["TIMESYS"].lower() 

142 used.append("TIMESYS") 

143 else: 

144 scale = "utc" 

145 if date_key is not None and self.is_key_ok(date_key): 

146 date_str = self._header[date_key] 

147 value = self._from_fits_date_string(date_str, scale=scale) 

148 used.append(date_key) 

149 elif self.is_key_ok(mjd_key): 

150 assert mjd_key is not None # for mypy (is_key_ok checks this) 

151 value = Time(self._header[mjd_key], scale=scale, format="mjd") 

152 used.append(mjd_key) 

153 else: 

154 value = None 

155 self._used_these_cards(*used) 

156 return value 

157 

158 @cache_translation 

159 def to_datetime_begin(self) -> Time: 

160 """Calculate start time of observation. 

161 

162 Uses FITS standard ``MJD-OBS`` or ``DATE-OBS``, in conjunction 

163 with the ``TIMESYS`` header. 

164 

165 Returns 

166 ------- 

167 start_time : `astropy.time.Time` 

168 Time corresponding to the start of the observation. 

169 """ 

170 return self._from_fits_date("DATE-OBS", mjd_key="MJD-OBS") 

171 

172 @cache_translation 

173 def to_datetime_end(self) -> Time: 

174 """Calculate end time of observation. 

175 

176 Uses FITS standard ``MJD-END`` or ``DATE-END``, in conjunction 

177 with the ``TIMESYS`` header. 

178 

179 Returns 

180 ------- 

181 start_time : `astropy.time.Time` 

182 Time corresponding to the end of the observation. 

183 """ 

184 return self._from_fits_date("DATE-END", mjd_key="MJD-END") 

185 

186 @cache_translation 

187 def to_location(self) -> EarthLocation: 

188 """Calculate the observatory location. 

189 

190 Uses FITS standard ``OBSGEO-`` headers. 

191 

192 Returns 

193 ------- 

194 location : `astropy.coordinates.EarthLocation` 

195 An object representing the location of the telescope. 

196 """ 

197 cards = [f"OBSGEO-{c}" for c in ("X", "Y", "Z")] 

198 coords = [self._header[c] for c in cards] 

199 value = EarthLocation.from_geocentric(*coords, unit=u.m) 

200 self._used_these_cards(*cards) 

201 return value