Coverage for python/astro_metadata_translator/translators/suprimecam.py: 48%
112 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-10-25 15:15 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-10-25 15:15 +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: 31 ↛ 32line 31 didn't jump to line 32, because the condition on line 31 was never true
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", dict(unit=u.K)),
62 "pressure": ("OUT-PRS", dict(unit=u.hPa)),
63 "exposure_time": ("EXPTIME", dict(unit=u.s)),
64 "dark_time": ("EXPTIME", dict(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
126 value = self._from_fits_date_string(
127 self._header["DATE-OBS"], time_str=self._header["UT-STR"], scale="utc"
128 )
129 self._used_these_cards("DATE-OBS", "UT-STR")
130 return value
132 @cache_translation
133 def to_datetime_end(self) -> astropy.time.Time:
134 # Docstring will be inherited. Property defined in properties.py
135 # We know it is UTC
136 value = self._from_fits_date_string(
137 self._header["DATE-OBS"], time_str=self._header["UT-END"], scale="utc"
138 )
139 self._used_these_cards("DATE-OBS", "UT-END")
141 # Sometimes the end time is less than the begin time plus the
142 # exposure time so we have to check for that.
143 exposure_time = self.to_exposure_time()
144 datetime_begin = self.to_datetime_begin()
145 exposure_end = datetime_begin + exposure_time
146 if value < exposure_end:
147 value = exposure_end
149 return value
151 @cache_translation
152 def to_exposure_id(self) -> int:
153 """Calculate unique exposure integer for this observation.
155 Returns
156 -------
157 visit : `int`
158 Integer uniquely identifying this exposure.
159 """
160 exp_id = self._header["EXP-ID"].strip()
161 m = re.search(r"^SUP[A-Z](\d{7})0$", exp_id)
162 if not m:
163 raise RuntimeError(f"{self._log_prefix}: Unable to interpret EXP-ID: {exp_id}")
164 exposure = int(m.group(1))
165 if int(exposure) == 0:
166 # Don't believe it
167 frame_id = self._header["FRAMEID"].strip()
168 m = re.search(r"^SUP[A-Z](\d{7})\d{1}$", frame_id)
169 if not m:
170 raise RuntimeError(f"{self._log_prefix}: Unable to interpret FRAMEID: {frame_id}")
171 exposure = int(m.group(1))
172 self._used_these_cards("EXP-ID", "FRAMEID")
173 return exposure
175 @cache_translation
176 def to_visit_id(self) -> int:
177 """Calculate the unique integer ID for this visit.
179 Assumed to be identical to the exposure ID in this implementation.
181 Returns
182 -------
183 exp : `int`
184 Unique visit identifier.
185 """
186 return self.to_exposure_id()
188 @cache_translation
189 def to_observation_type(self) -> str:
190 """Calculate the observation type.
192 Returns
193 -------
194 typ : `str`
195 Observation type. Normalized to standard set.
196 """
197 obstype = self._header["DATA-TYP"].strip().lower()
198 self._used_these_cards("DATA-TYP")
199 if obstype == "object":
200 return "science"
201 return obstype
203 @cache_translation
204 def to_tracking_radec(self) -> SkyCoord:
205 # Docstring will be inherited. Property defined in properties.py
206 radec = SkyCoord(
207 self._header["RA2000"],
208 self._header["DEC2000"],
209 frame="icrs",
210 unit=(u.hourangle, u.deg),
211 obstime=self.to_datetime_begin(),
212 location=self.to_location(),
213 )
214 self._used_these_cards("RA2000", "DEC2000")
215 return radec
217 @cache_translation
218 def to_altaz_begin(self) -> astropy.coordinates.AltAz:
219 # Docstring will be inherited. Property defined in properties.py
220 return altaz_from_degree_headers(self, (("ALTITUDE", "AZIMUTH"),), self.to_datetime_begin())
222 @cache_translation
223 def to_boresight_rotation_angle(self) -> Angle:
224 # Docstring will be inherited. Property defined in properties.py
225 angle = Angle(self.quantity_from_card("INR-STR", u.deg))
226 angle = angle.wrap_at("360d")
227 return angle
229 @cache_translation
230 def to_detector_exposure_id(self) -> int:
231 # Docstring will be inherited. Property defined in properties.py
232 return self.to_exposure_id() * 10 + self.to_detector_num()
234 @cache_translation
235 def to_detector_name(self) -> str:
236 # Docstring will be inherited. Property defined in properties.py
237 # See https://subarutelescope.org/Observing/Instruments/SCam/ccd.html
238 num = self.to_detector_num()
240 names = (
241 "nausicaa",
242 "kiki",
243 "fio",
244 "sophie",
245 "sheeta",
246 "satsuki",
247 "chihiro",
248 "clarisse",
249 "ponyo",
250 "san",
251 )
253 return names[num]