Coverage for python / astro_metadata_translator / translators / visit_info.py: 53%
70 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-17 08:50 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-17 08:50 +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 afw VisitInfo headers."""
14from __future__ import annotations
16__all__ = ("VisitInfoTranslator",)
18import logging
19from collections.abc import Mapping, MutableMapping
20from typing import TYPE_CHECKING, Any
22import astropy.time
23import astropy.units as u
24from astropy.coordinates import EarthLocation
26from ..translator import cache_translation
27from .fits import FitsTranslator
28from .helpers import altaz_from_degree_headers, tracking_from_degree_headers
30if TYPE_CHECKING:
31 import astropy.coordinates
33log = logging.getLogger(__name__)
36class VisitInfoTranslator(FitsTranslator):
37 """Metadata translator for afw VisitInfo serialization.
39 VisitInfo is only defined for on-sky observations.
41 VisitInfo does not encode the following properties:
43 * observing_day
44 * detector_serial
45 * detector_unique_name
46 * detector_group
47 * detector_name
49 Some values can be found if there is butler provenance:
51 * detector_num
52 * visit_id
53 """
55 name = "VisitInfo"
56 """Name of this translation class"""
58 supported_instrument = None
59 """Does not correspond to a single instrument."""
61 default_resource_root = None
62 """Corrections are not supported by this translator."""
64 _const_map = {
65 "detector_group": None,
66 "detector_unique_name": None,
67 "detector_serial": None,
68 "detector_exposure_id": None,
69 "detector_name": None,
70 "physical_filter": None,
71 }
73 _trivial_map: dict[str, str | list[str] | tuple[Any, ...]] = {
74 "exposure_time": ("EXPTIME", {"unit": u.s}),
75 "dark_time": ("DARKTIME", {"unit": u.s}),
76 "boresight_airmass": "BORE-AIRMASS",
77 "boresight_rotation_angle": ("BORE-ROTANG", {"unit": u.deg}),
78 "observation_id": "IDNUM",
79 "exposure_id": "IDNUM",
80 "object": "OBJECT",
81 "science_program": "PROGRAM",
82 "telescope": ("TELESCOP", {"default": "Unknown"}),
83 "instrument": "INSTRUMENT",
84 "relative_humidity": "HUMIDITY",
85 "temperature": ("AIRTEMP", {"unit": u.deg_C}),
86 "pressure": ("AIRPRESS", {"unit": u.Pa}),
87 "has_simulated_content": "HAS-SIMULATED-CONTENT",
88 "observation_type": "OBSTYPE",
89 "focus_z": ("FOCUSZ", {"unit": u.mm}),
90 "observation_reason": "REASON",
91 # Rely on butler provenance.
92 "detector_num": "LSST BUTLER DATAID DETECTOR",
93 }
95 @classmethod
96 def can_translate(cls, header: Mapping[str, Any], filename: str | None = None) -> bool:
97 """Indicate whether this translation class can translate the
98 supplied header.
100 Always returns `False`. This translator has to be selected explicitly
101 from context.
103 Parameters
104 ----------
105 header : `dict`-like
106 Header to convert to standardized form.
107 filename : `str`, optional
108 Name of file being translated.
110 Returns
111 -------
112 can : `bool`
113 `True` if the header is recognized by this class. `False`
114 otherwise.
115 """
116 return False
118 @cache_translation
119 def to_observation_counter(self) -> int:
120 """Return the exposure ID as proxy for counter.
122 Some exposure IDs encode a year and counter in the integer, others
123 simply have an incrementing counter
125 Returns
126 -------
127 sequence : `int`
128 The observation counter.
129 """
130 return self.to_exposure_id()
132 @cache_translation
133 def to_boresight_rotation_coord(self) -> str:
134 # Docstring will be inherited.
135 if self.is_key_ok("ROTTYPE"):
136 self._used_these_cards("ROTTYPE")
137 return self._header["ROTTYPE"].lower()
138 return "unknown"
140 @cache_translation
141 def to_visit_id(self) -> int:
142 # Docstring will be inherited. Property defined in properties.py
143 # This can be found in butler provenance in some cases.
144 # It is not generally used though and visit_id is effectively
145 # deprecated.
146 prov_key = "LSST BUTLER DATAID VISIT"
147 if self.is_key_ok(prov_key):
148 self._used_these_cards(prov_key)
149 return self._header[prov_key]
150 return self.to_exposure_id()
152 @cache_translation
153 def to_datetime_begin(self) -> astropy.time.Time | None:
154 if self.is_key_ok("DATE-AVG"):
155 date_avg = self._from_fits_date("DATE-AVG", scale="tai")
156 self._used_these_cards("DATE-AVG")
157 return date_avg - (self.to_exposure_time() / 2.0)
158 return None
160 @cache_translation
161 def to_datetime_end(self) -> astropy.time.Time:
162 # Docstring will be inherited. Property defined in properties.py
163 datetime_end = self.to_datetime_begin()
164 if datetime_end is not None:
165 datetime_end = self.to_datetime_begin() + self.to_exposure_time()
166 return datetime_end
168 @cache_translation
169 def to_location(self) -> astropy.coordinates.EarthLocation:
170 """Calculate the observatory location.
172 Returns
173 -------
174 location : `astropy.coordinates.EarthLocation`
175 An object representing the location of the telescope.
176 """
177 # OBS-LONG is east positive for VisitInfo.
178 lon = self._header["OBS-LONG"]
179 value = EarthLocation.from_geodetic(lon, self._header["OBS-LAT"], self._header["OBS-ELEV"])
180 self._used_these_cards("OBS-LONG", "OBS-LAT", "OBS-ELEV")
181 return value
183 @cache_translation
184 def to_tracking_radec(self) -> astropy.coordinates.SkyCoord | None:
185 # Docstring will be inherited. Property defined in properties.py
186 radecpairs = (("BORE-RA", "BORE-DEC"),)
187 return tracking_from_degree_headers(self, "ICRS", radecpairs, unit=(u.deg, u.deg))
189 @cache_translation
190 def to_altaz_begin(self) -> astropy.coordinates.AltAz | None:
191 # Docstring will be inherited. Property defined in properties.py
192 return altaz_from_degree_headers(self, (("BORE-ALT", "BORE-AZ"),), self.to_datetime_begin())
194 @classmethod
195 def fix_header(
196 cls, header: MutableMapping[str, Any], instrument: str, obsid: str, filename: str | None = None
197 ) -> bool:
198 """Fix DECam headers.
200 Parameters
201 ----------
202 header : `dict`
203 The header to update. Updates are in place.
204 instrument : `str`
205 The name of the instrument.
206 obsid : `str`
207 Unique observation identifier associated with this header.
208 Will always be provided.
209 filename : `str`, optional
210 Filename associated with this header. May not be set since headers
211 can be fixed independently of any filename being known.
213 Returns
214 -------
215 modified : `bool`
216 Returns `True` if the header was updated.
218 Notes
219 -----
220 No fixes are applied for VisitInfo at this time.
221 """
222 modified = False
223 return modified