Coverage for python / astro_metadata_translator / translators / suprimecam.py: 34%
117 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-26 08:50 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-26 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 SuprimeCam FITS headers."""
14from __future__ import annotations
16__all__ = ("SuprimeCamTranslator",)
18import logging
19import posixpath
20import re
21from collections.abc import Mapping
22from typing import TYPE_CHECKING, Any
24import astropy.units as u
25from astropy.coordinates import Angle, SkyCoord
27from ..translator import CORRECTIONS_RESOURCE_ROOT, cache_translation
28from .helpers import altaz_from_degree_headers
29from .subaru import SubaruTranslator
31if TYPE_CHECKING:
32 import astropy.coordinates
33 import astropy.time
35log = logging.getLogger(__name__)
38class SuprimeCamTranslator(SubaruTranslator):
39 """Metadata translator for HSC standard headers."""
41 name = "SuprimeCam"
42 """Name of this translation class"""
44 supported_instrument = "SuprimeCam"
45 """Supports the SuprimeCam instrument."""
47 default_resource_root = posixpath.join(CORRECTIONS_RESOURCE_ROOT, "SuprimeCam")
48 """Default resource path root to use to locate header correction files."""
50 _const_map = {"boresight_rotation_coord": "unknown", "detector_group": None}
51 """Constant mappings"""
53 _trivial_map: dict[str, str | list[str] | tuple[Any, ...]] = {
54 "observation_id": "EXP-ID",
55 "object": "OBJECT",
56 "science_program": "PROP-ID",
57 "detector_num": "DET-ID",
58 "detector_serial": "DETECTOR", # DETECTOR is the "call name"
59 "boresight_airmass": "AIRMASS",
60 "relative_humidity": "OUT-HUM",
61 "temperature": ("OUT-TMP", {"unit": u.K}),
62 "pressure": ("OUT-PRS", {"unit": u.hPa}),
63 "exposure_time": ("EXPTIME", {"unit": u.s}),
64 "dark_time": ("EXPTIME", {"unit": u.s}), # Assume same as exposure time
65 }
66 """One-to-one mappings"""
68 # Zero point for SuprimeCam dates: 2004-01-01
69 _DAY0 = 53005
71 @classmethod
72 def can_translate(cls, header: Mapping[str, Any], filename: str | None = None) -> bool:
73 """Indicate whether this translation class can translate the
74 supplied header.
76 Parameters
77 ----------
78 header : `dict`-like
79 Header to convert to standardized form.
80 filename : `str`, optional
81 Name of file being translated.
83 Returns
84 -------
85 can : `bool`
86 `True` if the header is recognized by this class. `False`
87 otherwise.
88 """
89 if "INSTRUME" in header:
90 return header["INSTRUME"] == "SuprimeCam"
92 for k in ("EXP-ID", "FRAMEID"):
93 if cls.is_keyword_defined(header, k):
94 if header[k].startswith("SUP"):
95 return True
96 return False
98 def _get_adjusted_mjd(self) -> int:
99 """Calculate the modified julian date offset from reference day.
101 Returns
102 -------
103 offset : `int`
104 Offset day count from reference day.
105 """
106 mjd = self._header["MJD"]
107 self._used_these_cards("MJD")
108 return int(mjd) - self._DAY0
110 @cache_translation
111 def to_physical_filter(self) -> str:
112 # Docstring will be inherited. Property defined in properties.py
113 value = self._header["FILTER01"].strip().upper()
114 self._used_these_cards("FILTER01")
115 # Map potential "unknown" values to standard form
116 if value in {"UNRECOGNIZED", "UNRECOGNISED", "NOTSET", "UNKNOWN"}:
117 value = "unknown"
118 elif value == "NONE":
119 value = "empty"
120 return value
122 @cache_translation
123 def to_datetime_begin(self) -> astropy.time.Time:
124 # Docstring will be inherited. Property defined in properties.py
125 # We know it is UTC. Also HSC date are non-standard with DATE-OBS
126 # only being YYYY-MM-DD.
127 value = None
128 if self.is_key_ok("DATE-OBS"):
129 # This should not happen for raw data but might happen for
130 # visit images.
131 value = self._from_fits_date_string(
132 self._header["DATE-OBS"], time_str=self._header["UT-STR"], scale="utc"
133 )
134 self._used_these_cards("DATE-OBS", "UT-STR")
135 else:
136 # Fallback to standard FITS in case AVG is present.
137 value = super().to_datetime_begin()
139 if value is None:
140 raise KeyError("Unable to find any usable date headers to calculate datetime_begin")
142 return value
144 @cache_translation
145 def to_datetime_end(self) -> astropy.time.Time | None:
146 # Docstring will be inherited. Property defined in properties.py
147 # We know it is UTC.
149 # For validation.
150 exposure_time = self.to_exposure_time()
151 datetime_begin = self.to_datetime_begin()
153 # This should be there for raws but maybe not for visits.
154 value = None
155 if self.is_key_ok("DATE-OBS"):
156 value = self._from_fits_date_string(
157 self._header["DATE-OBS"], time_str=self._header["UT-END"], scale="utc"
158 )
159 self._used_these_cards("DATE-OBS", "UT-END")
160 elif datetime_begin is not None and exposure_time is not None:
161 value = datetime_begin + exposure_time
163 # Sometimes the end time is less than the begin time plus the
164 # exposure time so we have to check for that.
165 exposure_end = datetime_begin + exposure_time
166 if value < exposure_end:
167 value = exposure_end
169 return value
171 @cache_translation
172 def to_exposure_id(self) -> int:
173 """Calculate unique exposure integer for this observation.
175 Returns
176 -------
177 visit : `int`
178 Integer uniquely identifying this exposure.
179 """
180 exp_id = self._header["EXP-ID"].strip()
181 m = re.search(r"^SUP[A-Z](\d{7})0$", exp_id)
182 if not m:
183 raise RuntimeError(f"{self._log_prefix}: Unable to interpret EXP-ID: {exp_id}")
184 exposure = int(m.group(1))
185 if int(exposure) == 0:
186 # Don't believe it
187 frame_id = self._header["FRAMEID"].strip()
188 m = re.search(r"^SUP[A-Z](\d{7})\d{1}$", frame_id)
189 if not m:
190 raise RuntimeError(f"{self._log_prefix}: Unable to interpret FRAMEID: {frame_id}")
191 exposure = int(m.group(1))
192 self._used_these_cards("EXP-ID", "FRAMEID")
193 return exposure
195 @cache_translation
196 def to_visit_id(self) -> int:
197 """Calculate the unique integer ID for this visit.
199 Assumed to be identical to the exposure ID in this implementation.
201 Returns
202 -------
203 exp : `int`
204 Unique visit identifier.
205 """
206 return self.to_exposure_id()
208 @cache_translation
209 def to_observation_type(self) -> str:
210 """Calculate the observation type.
212 Returns
213 -------
214 typ : `str`
215 Observation type. Normalized to standard set.
216 """
217 obstype = self._header["DATA-TYP"].strip().lower()
218 self._used_these_cards("DATA-TYP")
219 if obstype == "object":
220 return "science"
221 return obstype
223 @cache_translation
224 def to_tracking_radec(self) -> SkyCoord:
225 # Docstring will be inherited. Property defined in properties.py
226 radec = SkyCoord(
227 self._header["RA2000"],
228 self._header["DEC2000"],
229 frame="icrs",
230 unit=(u.hourangle, u.deg),
231 obstime=self.to_datetime_begin(),
232 location=self.to_location(),
233 )
234 self._used_these_cards("RA2000", "DEC2000")
235 return radec
237 @cache_translation
238 def to_altaz_begin(self) -> astropy.coordinates.AltAz | None:
239 # Docstring will be inherited. Property defined in properties.py
240 return altaz_from_degree_headers(self, (("ALTITUDE", "AZIMUTH"),), self.to_datetime_begin())
242 @cache_translation
243 def to_boresight_rotation_angle(self) -> Angle:
244 # Docstring will be inherited. Property defined in properties.py
245 angle = Angle(self.quantity_from_card("INR-STR", u.deg))
246 angle.wrap_at("360d", inplace=True)
247 return angle
249 @cache_translation
250 def to_detector_exposure_id(self) -> int:
251 # Docstring will be inherited. Property defined in properties.py
252 return self.to_exposure_id() * 10 + self.to_detector_num()
254 @cache_translation
255 def to_detector_name(self) -> str:
256 # Docstring will be inherited. Property defined in properties.py
257 # See https://subarutelescope.org/Observing/Instruments/SCam/ccd.html
258 num = self.to_detector_num()
260 names = (
261 "nausicaa",
262 "kiki",
263 "fio",
264 "sophie",
265 "sheeta",
266 "satsuki",
267 "chihiro",
268 "clarisse",
269 "ponyo",
270 "san",
271 )
273 return names[num]