Coverage for python/astro_metadata_translator/translators/suprimecam.py: 39%
111 statements
« prev ^ index » next coverage.py v7.2.1, created at 2023-03-12 20:36 -0700
« prev ^ index » next coverage.py v7.2.1, created at 2023-03-12 20:36 -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 SuprimeCam FITS headers"""
14from __future__ import annotations
16__all__ = ("SuprimeCamTranslator",)
18import logging
19import posixpath
20import re
21from typing import TYPE_CHECKING, Any, Dict, List, MutableMapping, Optional, Tuple, Union
23import astropy.units as u
24from astropy.coordinates import Angle, SkyCoord
26from ..translator import CORRECTIONS_RESOURCE_ROOT, cache_translation
27from .helpers import altaz_from_degree_headers
28from .subaru import SubaruTranslator
30if TYPE_CHECKING: 30 ↛ 31line 30 didn't jump to line 31, because the condition on line 30 was never true
31 import astropy.coordinates
32 import astropy.time
34log = logging.getLogger(__name__)
37class SuprimeCamTranslator(SubaruTranslator):
38 """Metadata translator for HSC standard headers."""
40 name = "SuprimeCam"
41 """Name of this translation class"""
43 supported_instrument = "SuprimeCam"
44 """Supports the SuprimeCam instrument."""
46 default_resource_root = posixpath.join(CORRECTIONS_RESOURCE_ROOT, "SuprimeCam")
47 """Default resource path root to use to locate header correction files."""
49 _const_map = {"boresight_rotation_coord": "unknown", "detector_group": None}
50 """Constant mappings"""
52 _trivial_map: Dict[str, Union[str, List[str], Tuple[Any, ...]]] = {
53 "observation_id": "EXP-ID",
54 "object": "OBJECT",
55 "science_program": "PROP-ID",
56 "detector_num": "DET-ID",
57 "detector_serial": "DETECTOR", # DETECTOR is the "call name"
58 "boresight_airmass": "AIRMASS",
59 "relative_humidity": "OUT-HUM",
60 "temperature": ("OUT-TMP", dict(unit=u.K)),
61 "pressure": ("OUT-PRS", dict(unit=u.hPa)),
62 "exposure_time": ("EXPTIME", dict(unit=u.s)),
63 "dark_time": ("EXPTIME", dict(unit=u.s)), # Assume same as exposure time
64 }
65 """One-to-one mappings"""
67 # Zero point for SuprimeCam dates: 2004-01-01
68 _DAY0 = 53005
70 @classmethod
71 def can_translate(cls, header: MutableMapping[str, Any], filename: Optional[str] = None) -> bool:
72 """Indicate whether this translation class can translate the
73 supplied header.
75 Parameters
76 ----------
77 header : `dict`-like
78 Header to convert to standardized form.
79 filename : `str`, optional
80 Name of file being translated.
82 Returns
83 -------
84 can : `bool`
85 `True` if the header is recognized by this class. `False`
86 otherwise.
87 """
88 if "INSTRUME" in header:
89 return header["INSTRUME"] == "SuprimeCam"
91 for k in ("EXP-ID", "FRAMEID"):
92 if cls.is_keyword_defined(header, k):
93 if header[k].startswith("SUP"):
94 return True
95 return False
97 def _get_adjusted_mjd(self) -> int:
98 """Calculate the modified julian date offset from reference day
100 Returns
101 -------
102 offset : `int`
103 Offset day count from reference day.
104 """
105 mjd = self._header["MJD"]
106 self._used_these_cards("MJD")
107 return int(mjd) - self._DAY0
109 @cache_translation
110 def to_physical_filter(self) -> str:
111 # Docstring will be inherited. Property defined in properties.py
112 value = self._header["FILTER01"].strip().upper()
113 self._used_these_cards("FILTER01")
114 # Map potential "unknown" values to standard form
115 if value in {"UNRECOGNIZED", "UNRECOGNISED", "NOTSET", "UNKNOWN"}:
116 value = "unknown"
117 elif value == "NONE":
118 value = "empty"
119 return value
121 @cache_translation
122 def to_datetime_begin(self) -> astropy.time.Time:
123 # Docstring will be inherited. Property defined in properties.py
124 # We know it is UTC
125 value = self._from_fits_date_string(
126 self._header["DATE-OBS"], time_str=self._header["UT-STR"], scale="utc"
127 )
128 self._used_these_cards("DATE-OBS", "UT-STR")
129 return value
131 @cache_translation
132 def to_datetime_end(self) -> astropy.time.Time:
133 # Docstring will be inherited. Property defined in properties.py
134 # We know it is UTC
135 value = self._from_fits_date_string(
136 self._header["DATE-OBS"], time_str=self._header["UT-END"], scale="utc"
137 )
138 self._used_these_cards("DATE-OBS", "UT-END")
140 # Sometimes the end time is less than the begin time plus the
141 # exposure time so we have to check for that.
142 exposure_time = self.to_exposure_time()
143 datetime_begin = self.to_datetime_begin()
144 exposure_end = datetime_begin + exposure_time
145 if value < exposure_end:
146 value = exposure_end
148 return value
150 @cache_translation
151 def to_exposure_id(self) -> int:
152 """Calculate unique exposure integer for this observation
154 Returns
155 -------
156 visit : `int`
157 Integer uniquely identifying this exposure.
158 """
159 exp_id = self._header["EXP-ID"].strip()
160 m = re.search(r"^SUP[A-Z](\d{7})0$", exp_id)
161 if not m:
162 raise RuntimeError(f"{self._log_prefix}: Unable to interpret EXP-ID: {exp_id}")
163 exposure = int(m.group(1))
164 if int(exposure) == 0:
165 # Don't believe it
166 frame_id = self._header["FRAMEID"].strip()
167 m = re.search(r"^SUP[A-Z](\d{7})\d{1}$", frame_id)
168 if not m:
169 raise RuntimeError(f"{self._log_prefix}: Unable to interpret FRAMEID: {frame_id}")
170 exposure = int(m.group(1))
171 self._used_these_cards("EXP-ID", "FRAMEID")
172 return exposure
174 @cache_translation
175 def to_visit_id(self) -> int:
176 """Calculate the unique integer ID for this visit.
178 Assumed to be identical to the exposure ID in this implementation.
180 Returns
181 -------
182 exp : `int`
183 Unique visit identifier.
184 """
185 return self.to_exposure_id()
187 @cache_translation
188 def to_observation_type(self) -> str:
189 """Calculate the observation type.
191 Returns
192 -------
193 typ : `str`
194 Observation type. Normalized to standard set.
195 """
196 obstype = self._header["DATA-TYP"].strip().lower()
197 self._used_these_cards("DATA-TYP")
198 if obstype == "object":
199 return "science"
200 return obstype
202 @cache_translation
203 def to_tracking_radec(self) -> SkyCoord:
204 # Docstring will be inherited. Property defined in properties.py
205 radec = SkyCoord(
206 self._header["RA2000"],
207 self._header["DEC2000"],
208 frame="icrs",
209 unit=(u.hourangle, u.deg),
210 obstime=self.to_datetime_begin(),
211 location=self.to_location(),
212 )
213 self._used_these_cards("RA2000", "DEC2000")
214 return radec
216 @cache_translation
217 def to_altaz_begin(self) -> astropy.coordinates.AltAz:
218 # Docstring will be inherited. Property defined in properties.py
219 return altaz_from_degree_headers(self, (("ALTITUDE", "AZIMUTH"),), self.to_datetime_begin())
221 @cache_translation
222 def to_boresight_rotation_angle(self) -> Angle:
223 # Docstring will be inherited. Property defined in properties.py
224 angle = Angle(self.quantity_from_card("INR-STR", u.deg))
225 angle = angle.wrap_at("360d")
226 return angle
228 @cache_translation
229 def to_detector_exposure_id(self) -> int:
230 # Docstring will be inherited. Property defined in properties.py
231 return self.to_exposure_id() * 10 + self.to_detector_num()
233 @cache_translation
234 def to_detector_name(self) -> str:
235 # Docstring will be inherited. Property defined in properties.py
236 # See https://subarutelescope.org/Observing/Instruments/SCam/ccd.html
237 num = self.to_detector_num()
239 names = (
240 "nausicaa",
241 "kiki",
242 "fio",
243 "sophie",
244 "sheeta",
245 "satsuki",
246 "chihiro",
247 "clarisse",
248 "ponyo",
249 "san",
250 )
252 return names[num]