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

63 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-06 03:48 -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 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 # Direct translation from header key to standard form 

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

41 instrument="INSTRUME", 

42 telescope="TELESCOP", 

43 ) 

44 

45 @classmethod 

46 def can_translate(cls, header: Mapping[str, Any], filename: str | None = 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(cls, date_str: str, scale: str = "utc", time_str: str | None = None) -> Time: 

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

82 

83 Parameters 

84 ---------- 

85 date_str : `str` 

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

87 lookup in the header. 

88 scale : `str`, optional 

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

90 UTC. 

91 time_str : `str`, optional 

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

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

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

95 

96 Returns 

97 ------- 

98 date : `astropy.time.Time` 

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

100 """ 

101 if time_str is not None: 

102 date_str = f"{date_str[:10]}T{time_str}" 

103 

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

105 

106 def _from_fits_date( 

107 self, date_key: str, mjd_key: str | None = None, scale: str | None = None 

108 ) -> Time | None: 

109 """Calculate a date object from the named FITS header. 

110 

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

112 defaulting to UTC. Can be overridden since sometimes headers 

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

114 different time scales. 

115 

116 Parameters 

117 ---------- 

118 date_key : `str` 

119 The key in the header representing a standard FITS 

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

121 mjd_key : `str`, optional 

122 The key in the header representing a standard FITS MJD 

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

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

125 scale : `str`, optional 

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

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

128 `~astropy.time.Time`. 

129 

130 Returns 

131 ------- 

132 date : `astropy.time.Time` 

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

134 """ 

135 used = [] 

136 if scale is not None: 

137 pass 

138 elif self.is_key_ok("TIMESYS"): 

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

140 used.append("TIMESYS") 

141 else: 

142 scale = "utc" 

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

144 date_str = self._header[date_key] 

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

146 used.append(date_key) 

147 elif self.is_key_ok(mjd_key): 

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

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

150 used.append(mjd_key) 

151 else: 

152 value = None 

153 self._used_these_cards(*used) 

154 return value 

155 

156 @cache_translation 

157 def to_datetime_begin(self) -> Time | None: 

158 """Calculate start time of observation. 

159 

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

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

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

163 

164 Returns 

165 ------- 

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

167 Time corresponding to the start of the observation. Returns 

168 `None` if no date can be found. 

169 """ 

170 # Prefer -BEG over -OBS 

171 begin = None 

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

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

174 if begin is not None: 

175 break 

176 return begin 

177 

178 @cache_translation 

179 def to_datetime_end(self) -> Time: 

180 """Calculate end time of observation. 

181 

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

183 with the ``TIMESYS`` header. 

184 

185 Returns 

186 ------- 

187 start_time : `astropy.time.Time` 

188 Time corresponding to the end of the observation. 

189 """ 

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

191 

192 @cache_translation 

193 def to_location(self) -> EarthLocation: 

194 """Calculate the observatory location. 

195 

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

197 

198 Returns 

199 ------- 

200 location : `astropy.coordinates.EarthLocation` 

201 An object representing the location of the telescope. 

202 """ 

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

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

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

206 self._used_these_cards(*cards) 

207 return value