Coverage for python/astro_metadata_translator/translators/fits.py: 34%
63 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-28 02:59 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-28 02:59 -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.
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]
37 """
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 )
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.
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(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.
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.
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}"
104 return Time(date_str, format="isot", scale=scale)
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.
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.
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`.
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
156 @cache_translation
157 def to_datetime_begin(self) -> Time | None:
158 """Calculate start time of observation.
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.
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
178 @cache_translation
179 def to_datetime_end(self) -> Time:
180 """Calculate end time of observation.
182 Uses FITS standard ``MJD-END`` or ``DATE-END``, in conjunction
183 with the ``TIMESYS`` header.
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")
192 @cache_translation
193 def to_location(self) -> EarthLocation:
194 """Calculate the observatory location.
196 Uses FITS standard ``OBSGEO-`` headers.
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