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

63 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-21 10:02 +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"""Metadata translation code for standard FITS headers.""" 

13 

14from __future__ import annotations 

15 

16__all__ = ("FitsTranslator",) 

17 

18from collections.abc import Mapping 

19from typing import Any 

20 

21import astropy.units as u 

22from astropy.coordinates import EarthLocation 

23from astropy.time import Time 

24 

25from ..translator import MetadataTranslator, cache_translation 

26 

27 

28class FitsTranslator(MetadataTranslator): 

29 """Metadata translator for FITS standard headers. 

30 

31 Understands: 

32 

33 - DATE-OBS/MJD-OBS or DATE-BEG/MJD-BEG (-BEG is preferred). 

34 - INSTRUME 

35 - TELESCOP 

36 - OBSGEO-[X,Y,Z] 

37 

38 """ 

39 

40 # Direct translation from header key to standard form 

41 _trivial_map: dict[str, str | list[str] | tuple[Any, ...]] = dict( 

42 instrument="INSTRUME", 

43 telescope="TELESCOP", 

44 ) 

45 

46 @classmethod 

47 def can_translate(cls, header: Mapping[str, Any], filename: str | None = None) -> bool: 

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

49 supplied header. 

50 

51 Checks the instrument value and compares with the supported 

52 instruments in the class 

53 

54 Parameters 

55 ---------- 

56 header : `dict`-like 

57 Header to convert to standardized form. 

58 filename : `str`, optional 

59 Name of file being translated. 

60 

61 Returns 

62 ------- 

63 can : `bool` 

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

65 otherwise. 

66 """ 

67 if cls.supported_instrument is None: 

68 return False 

69 

70 # Protect against being able to always find a standard 

71 # header for instrument 

72 try: 

73 translator = cls(header, filename=filename) 

74 instrument = translator.to_instrument() 

75 except KeyError: 

76 return False 

77 

78 return instrument == cls.supported_instrument 

79 

80 @classmethod 

81 def _from_fits_date_string(cls, date_str: str, scale: str = "utc", time_str: str | None = None) -> 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 = f"{date_str[:10]}T{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: str | None = None, scale: str | None = None 

109 ) -> Time | None: 

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 | None: 

159 """Calculate start time of observation. 

160 

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

162 with the ``TIMESYS`` header. Will fallback to using ``MJD-OBS`` 

163 or ``DATE-OBS`` if the ``-BEG`` variants are not found. 

164 

165 Returns 

166 ------- 

167 start_time : `astropy.time.Time` or `None` 

168 Time corresponding to the start of the observation. Returns 

169 `None` if no date can be found. 

170 """ 

171 # Prefer -BEG over -OBS 

172 begin = None 

173 for suffix in ("BEG", "OBS"): 

174 begin = self._from_fits_date(f"DATE-{suffix}", mjd_key=f"MJD-{suffix}") 

175 if begin is not None: 

176 break 

177 return begin 

178 

179 @cache_translation 

180 def to_datetime_end(self) -> Time: 

181 """Calculate end time of observation. 

182 

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

184 with the ``TIMESYS`` header. 

185 

186 Returns 

187 ------- 

188 start_time : `astropy.time.Time` 

189 Time corresponding to the end of the observation. 

190 """ 

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

192 

193 @cache_translation 

194 def to_location(self) -> EarthLocation: 

195 """Calculate the observatory location. 

196 

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

198 

199 Returns 

200 ------- 

201 location : `astropy.coordinates.EarthLocation` 

202 An object representing the location of the telescope. 

203 """ 

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

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

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

207 self._used_these_cards(*cards) 

208 return value