Coverage for python/astro_metadata_translator/translators/fits.py: 29%
57 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-08 02:23 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-08 02:23 -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.
12"""Metadata translation code for standard FITS headers"""
14from __future__ import annotations
16__all__ = ("FitsTranslator",)
18from typing import Any, Dict, List, MutableMapping, Optional, Tuple, Union
20import astropy.units as u
21from astropy.coordinates import EarthLocation
22from astropy.time import Time
24from ..translator import MetadataTranslator, cache_translation
27class FitsTranslator(MetadataTranslator):
28 """Metadata translator for FITS standard headers.
30 Understands:
32 - DATE-OBS
33 - INSTRUME
34 - TELESCOP
35 - OBSGEO-[X,Y,Z]
37 """
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 )
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.
50 Checks the instrument value and compares with the supported
51 instruments in the class
53 Parameters
54 ----------
55 header : `dict`-like
56 Header to convert to standardized form.
57 filename : `str`, optional
58 Name of file being translated.
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
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
77 return instrument == cls.supported_instrument
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
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.
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)
106 return Time(date_str, format="isot", scale=scale)
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
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.
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`.
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
158 @cache_translation
159 def to_datetime_begin(self) -> Time:
160 """Calculate start time of observation.
162 Uses FITS standard ``MJD-OBS`` or ``DATE-OBS``, in conjunction
163 with the ``TIMESYS`` header.
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")
172 @cache_translation
173 def to_datetime_end(self) -> Time:
174 """Calculate end time of observation.
176 Uses FITS standard ``MJD-END`` or ``DATE-END``, in conjunction
177 with the ``TIMESYS`` header.
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")
186 @cache_translation
187 def to_location(self) -> EarthLocation:
188 """Calculate the observatory location.
190 Uses FITS standard ``OBSGEO-`` headers.
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