Coverage for python/astro_metadata_translator/translators/fits.py: 31%
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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", telescope="TELESCOP"
42 )
44 @classmethod
45 def can_translate(cls, header: MutableMapping[str, Any], filename: Optional[str] = None) -> bool:
46 """Indicate whether this translation class can translate the
47 supplied header.
49 Checks the instrument value and compares with the supported
50 instruments in the class
52 Parameters
53 ----------
54 header : `dict`-like
55 Header to convert to standardized form.
56 filename : `str`, optional
57 Name of file being translated.
59 Returns
60 -------
61 can : `bool`
62 `True` if the header is recognized by this class. `False`
63 otherwise.
64 """
65 if cls.supported_instrument is None:
66 return False
68 # Protect against being able to always find a standard
69 # header for instrument
70 try:
71 translator = cls(header, filename=filename)
72 instrument = translator.to_instrument()
73 except KeyError:
74 return False
76 return instrument == cls.supported_instrument
78 @classmethod
79 def _from_fits_date_string(
80 cls, date_str: str, scale: str = "utc", time_str: Optional[str] = None
81 ) -> 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 = "{}T{}".format(date_str[:10], time_str)
105 return Time(date_str, format="isot", scale=scale)
107 def _from_fits_date(
108 self, date_key: str, mjd_key: Optional[str] = None, scale: Optional[str] = None
109 ) -> Time:
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:
159 """Calculate start time of observation.
161 Uses FITS standard ``MJD-OBS`` or ``DATE-OBS``, in conjunction
162 with the ``TIMESYS`` header.
164 Returns
165 -------
166 start_time : `astropy.time.Time`
167 Time corresponding to the start of the observation.
168 """
169 return self._from_fits_date("DATE-OBS", mjd_key="MJD-OBS")
171 @cache_translation
172 def to_datetime_end(self) -> Time:
173 """Calculate end time of observation.
175 Uses FITS standard ``MJD-END`` or ``DATE-END``, in conjunction
176 with the ``TIMESYS`` header.
178 Returns
179 -------
180 start_time : `astropy.time.Time`
181 Time corresponding to the end of the observation.
182 """
183 return self._from_fits_date("DATE-END", mjd_key="MJD-END")
185 @cache_translation
186 def to_location(self) -> EarthLocation:
187 """Calculate the observatory location.
189 Uses FITS standard ``OBSGEO-`` headers.
191 Returns
192 -------
193 location : `astropy.coordinates.EarthLocation`
194 An object representing the location of the telescope.
195 """
196 cards = [f"OBSGEO-{c}" for c in ("X", "Y", "Z")]
197 coords = [self._header[c] for c in cards]
198 value = EarthLocation.from_geocentric(*coords, unit=u.m)
199 self._used_these_cards(*cards)
200 return value