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

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

57 statements  

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", telescope="TELESCOP" 

42 ) 

43 

44 @classmethod 

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

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

47 supplied header. 

48 

49 Checks the instrument value and compares with the supported 

50 instruments in the class 

51 

52 Parameters 

53 ---------- 

54 header : `dict`-like 

55 Header to convert to standardized form. 

56 filename : `str`, optional 

57 Name of file being translated. 

58 

59 Returns 

60 ------- 

61 can : `bool` 

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

63 otherwise. 

64 """ 

65 if cls.supported_instrument is None: 

66 return False 

67 

68 # Protect against being able to always find a standard 

69 # header for instrument 

70 try: 

71 translator = cls(header, filename=filename) 

72 instrument = translator.to_instrument() 

73 except KeyError: 

74 return False 

75 

76 return instrument == cls.supported_instrument 

77 

78 @classmethod 

79 def _from_fits_date_string( 

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

81 ) -> Time: 

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

83 

84 Parameters 

85 ---------- 

86 date_str : `str` 

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

88 lookup in the header. 

89 scale : `str`, optional 

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

91 UTC. 

92 time_str : `str`, optional 

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

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

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

96 

97 Returns 

98 ------- 

99 date : `astropy.time.Time` 

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

101 """ 

102 if time_str is not None: 

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

104 

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

106 

107 def _from_fits_date( 

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

109 ) -> Time: 

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

111 

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

113 defaulting to UTC. Can be overridden since sometimes headers 

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

115 different time scales. 

116 

117 Parameters 

118 ---------- 

119 date_key : `str` 

120 The key in the header representing a standard FITS 

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

122 mjd_key : `str`, optional 

123 The key in the header representing a standard FITS MJD 

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

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

126 scale : `str`, optional 

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

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

129 `~astropy.time.Time`. 

130 

131 Returns 

132 ------- 

133 date : `astropy.time.Time` 

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

135 """ 

136 used = [] 

137 if scale is not None: 

138 pass 

139 elif self.is_key_ok("TIMESYS"): 

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

141 used.append("TIMESYS") 

142 else: 

143 scale = "utc" 

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

145 date_str = self._header[date_key] 

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

147 used.append(date_key) 

148 elif self.is_key_ok(mjd_key): 

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

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

151 used.append(mjd_key) 

152 else: 

153 value = None 

154 self._used_these_cards(*used) 

155 return value 

156 

157 @cache_translation 

158 def to_datetime_begin(self) -> Time: 

159 """Calculate start time of observation. 

160 

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

162 with the ``TIMESYS`` header. 

163 

164 Returns 

165 ------- 

166 start_time : `astropy.time.Time` 

167 Time corresponding to the start of the observation. 

168 """ 

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

170 

171 @cache_translation 

172 def to_datetime_end(self) -> Time: 

173 """Calculate end time of observation. 

174 

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

176 with the ``TIMESYS`` header. 

177 

178 Returns 

179 ------- 

180 start_time : `astropy.time.Time` 

181 Time corresponding to the end of the observation. 

182 """ 

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

184 

185 @cache_translation 

186 def to_location(self) -> EarthLocation: 

187 """Calculate the observatory location. 

188 

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

190 

191 Returns 

192 ------- 

193 location : `astropy.coordinates.EarthLocation` 

194 An object representing the location of the telescope. 

195 """ 

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

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

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

199 self._used_these_cards(*cards) 

200 return value