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
« 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.
12"""Metadata translation code for standard FITS headers."""
14from __future__ import annotations
16__all__ = ("FitsTranslator",)
18from collections.abc import Mapping
19from typing import Any
21import astropy.units as u
22from astropy.coordinates import EarthLocation
23from astropy.time import Time
25from ..translator import MetadataTranslator, cache_translation
28class FitsTranslator(MetadataTranslator):
29 """Metadata translator for FITS standard headers.
31 Understands:
33 - DATE-OBS/MJD-OBS or DATE-BEG/MJD-BEG (-BEG is preferred).
34 - INSTRUME
35 - TELESCOP
36 - OBSGEO-[X,Y,Z]
38 """
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 )
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.
51 Checks the instrument value and compares with the supported
52 instruments in the class
54 Parameters
55 ----------
56 header : `dict`-like
57 Header to convert to standardized form.
58 filename : `str`, optional
59 Name of file being translated.
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
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
78 return instrument == cls.supported_instrument
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.
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.
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}"
105 return Time(date_str, format="isot", scale=scale)
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.
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.
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`.
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
157 @cache_translation
158 def to_datetime_begin(self) -> Time | None:
159 """Calculate start time of observation.
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.
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
179 @cache_translation
180 def to_datetime_end(self) -> Time:
181 """Calculate end time of observation.
183 Uses FITS standard ``MJD-END`` or ``DATE-END``, in conjunction
184 with the ``TIMESYS`` header.
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")
193 @cache_translation
194 def to_location(self) -> EarthLocation:
195 """Calculate the observatory location.
197 Uses FITS standard ``OBSGEO-`` headers.
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