Coverage for python/astro_metadata_translator/translators/hsc.py: 49%
95 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-28 02:59 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-28 02:59 -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 HSC FITS headers."""
14from __future__ import annotations
16__all__ = ("HscTranslator",)
18import logging
19import posixpath
20import re
21from collections.abc import Mapping
22from typing import Any
24import astropy.units as u
25from astropy.coordinates import Angle
27from ..translator import CORRECTIONS_RESOURCE_ROOT, cache_translation
28from .suprimecam import SuprimeCamTranslator
30log = logging.getLogger(__name__)
33class HscTranslator(SuprimeCamTranslator):
34 """Metadata translator for HSC standard headers."""
36 name = "HSC"
37 """Name of this translation class"""
39 supported_instrument = "HSC"
40 """Supports the HSC instrument."""
42 default_resource_root = posixpath.join(CORRECTIONS_RESOURCE_ROOT, "HSC")
43 """Default resource path root to use to locate header correction files."""
45 _const_map = {"instrument": "HSC", "boresight_rotation_coord": "sky"}
46 """Hard wire HSC even though modern headers call it Hyper Suprime-Cam"""
48 _trivial_map = {
49 "detector_serial": "T_CCDSN",
50 }
51 """One-to-one mappings"""
53 # List from Hisanori Furusawa (2024-03-25).
54 _sky_observation_types: tuple[str, ...] = (
55 "science",
56 "object",
57 "standard_star",
58 "skyflat",
59 "focus",
60 "focusing",
61 "exp",
62 )
63 _non_sky_observation_types: tuple[str, ...] = ("dark", "bias", "agexp", "domeflat")
65 # Zero point for HSC dates: 2012-01-01 51544 -> 2000-01-01
66 _DAY0 = 55927
68 # CCD index mapping for commissioning run 2
69 _CCD_MAP_COMMISSIONING_2 = {
70 112: 106,
71 107: 105,
72 113: 107,
73 115: 109,
74 108: 110,
75 114: 108,
76 }
78 _DETECTOR_NUM_TO_UNIQUE_NAME = [
79 "1_53",
80 "1_54",
81 "1_55",
82 "1_56",
83 "1_42",
84 "1_43",
85 "1_44",
86 "1_45",
87 "1_46",
88 "1_47",
89 "1_36",
90 "1_37",
91 "1_38",
92 "1_39",
93 "1_40",
94 "1_41",
95 "0_30",
96 "0_29",
97 "0_28",
98 "1_32",
99 "1_33",
100 "1_34",
101 "0_27",
102 "0_26",
103 "0_25",
104 "0_24",
105 "1_00",
106 "1_01",
107 "1_02",
108 "1_03",
109 "0_23",
110 "0_22",
111 "0_21",
112 "0_20",
113 "1_04",
114 "1_05",
115 "1_06",
116 "1_07",
117 "0_19",
118 "0_18",
119 "0_17",
120 "0_16",
121 "1_08",
122 "1_09",
123 "1_10",
124 "1_11",
125 "0_15",
126 "0_14",
127 "0_13",
128 "0_12",
129 "1_12",
130 "1_13",
131 "1_14",
132 "1_15",
133 "0_11",
134 "0_10",
135 "0_09",
136 "0_08",
137 "1_16",
138 "1_17",
139 "1_18",
140 "1_19",
141 "0_07",
142 "0_06",
143 "0_05",
144 "0_04",
145 "1_20",
146 "1_21",
147 "1_22",
148 "1_23",
149 "0_03",
150 "0_02",
151 "0_01",
152 "0_00",
153 "1_24",
154 "1_25",
155 "1_26",
156 "1_27",
157 "0_34",
158 "0_33",
159 "0_32",
160 "1_28",
161 "1_29",
162 "1_30",
163 "0_41",
164 "0_40",
165 "0_39",
166 "0_38",
167 "0_37",
168 "0_36",
169 "0_47",
170 "0_46",
171 "0_45",
172 "0_44",
173 "0_43",
174 "0_42",
175 "0_56",
176 "0_55",
177 "0_54",
178 "0_53",
179 "0_31",
180 "1_35",
181 "0_35",
182 "1_31",
183 "1_48",
184 "1_51",
185 "1_52",
186 "1_57",
187 "0_57",
188 "0_52",
189 "0_51",
190 "0_48",
191 ]
193 @classmethod
194 def can_translate(cls, header: Mapping[str, Any], filename: str | None = None) -> bool:
195 """Indicate whether this translation class can translate the
196 supplied header.
198 There is no ``INSTRUME`` header in early HSC files, so this method
199 looks for HSC mentions in other headers. In more recent files the
200 instrument is called "Hyper Suprime-Cam".
202 Parameters
203 ----------
204 header : `dict`-like
205 Header to convert to standardized form.
206 filename : `str`, optional
207 Name of file being translated.
209 Returns
210 -------
211 can : `bool`
212 `True` if the header is recognized by this class. `False`
213 otherwise.
214 """
215 if "INSTRUME" in header:
216 return header["INSTRUME"] == "Hyper Suprime-Cam"
218 for k in ("EXP-ID", "FRAMEID"):
219 if cls.is_keyword_defined(header, k):
220 if header[k].startswith("HSC"):
221 return True
222 return False
224 @cache_translation
225 def to_exposure_id(self) -> int:
226 """Calculate unique exposure integer for this observation.
228 Returns
229 -------
230 visit : `int`
231 Integer uniquely identifying this exposure.
232 """
233 exp_id = self._header["EXP-ID"].strip()
234 m = re.search(r"^HSCE(\d{8})$", exp_id) # 2016-06-14 and new scheme
235 if m:
236 self._used_these_cards("EXP-ID")
237 return int(m.group(1))
239 # Fallback to old scheme
240 m = re.search(r"^HSC([A-Z])(\d{6})00$", exp_id)
241 if not m:
242 raise RuntimeError(f"{self._log_prefix}: Unable to interpret EXP-ID: {exp_id}")
243 letter, visit = m.groups()
244 visit = int(visit)
245 if visit == 0:
246 # Don't believe it
247 frame_id = self._header["FRAMEID"].strip()
248 m = re.search(r"^HSC([A-Z])(\d{6})\d{2}$", frame_id)
249 if not m:
250 raise RuntimeError(f"{self._log_prefix}: Unable to interpret FRAMEID: {frame_id}")
251 letter, visit = m.groups()
252 visit = int(visit)
253 if visit % 2: # Odd?
254 visit -= 1
255 self._used_these_cards("EXP-ID", "FRAMEID")
256 return visit + 1000000 * (ord(letter) - ord("A"))
258 @cache_translation
259 def to_boresight_rotation_angle(self) -> Angle:
260 # Docstring will be inherited. Property defined in properties.py
261 # Rotation angle formula determined empirically from visual inspection
262 # of HSC images. See DM-9111.
263 angle = Angle(270.0 * u.deg) - Angle(self.quantity_from_card("INST-PA", u.deg))
264 angle = angle.wrap_at("360d")
265 return angle
267 @cache_translation
268 def to_detector_num(self) -> int:
269 """Calculate the detector number.
271 Focus CCDs were numbered incorrectly in the readout software during
272 commissioning run 2. This method maps to the correct ones.
274 Returns
275 -------
276 num : `int`
277 Detector number.
278 """
279 ccd = super().to_detector_num()
280 try:
281 tjd = self._get_adjusted_mjd()
282 except Exception:
283 return ccd
285 if tjd > 390 and tjd < 405:
286 ccd = self._CCD_MAP_COMMISSIONING_2.get(ccd, ccd)
288 return ccd
290 @cache_translation
291 def to_detector_exposure_id(self) -> int:
292 # Docstring will be inherited. Property defined in properties.py
293 return self.to_exposure_id() * 200 + self.to_detector_num()
295 @cache_translation
296 def to_detector_group(self) -> str:
297 # Docstring will be inherited. Property defined in properties.py
298 unique = self.to_detector_unique_name()
299 return unique.split("_")[0]
301 @cache_translation
302 def to_detector_unique_name(self) -> str:
303 # Docstring will be inherited. Property defined in properties.py
304 # Mapping from number to unique name is defined solely in camera
305 # geom files.
306 # There is no header for it.
307 num = self.to_detector_num()
308 return self._DETECTOR_NUM_TO_UNIQUE_NAME[num]
310 @cache_translation
311 def to_detector_name(self) -> str:
312 # Docstring will be inherited. Property defined in properties.py
313 # Name is defined from unique name
314 unique = self.to_detector_unique_name()
315 return unique.split("_")[1]
317 @cache_translation
318 def to_focus_z(self) -> u.Quantity:
319 # Docstring will be inherited. Property defined in properties.py
320 foc_val = self._header["FOC-VAL"]
321 return foc_val * u.mm