Coverage for python/astro_metadata_translator/translators/hsc.py: 48%
93 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-12 11:52 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-12 11:52 +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 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 # Zero point for HSC dates: 2012-01-01 51544 -> 2000-01-01
54 _DAY0 = 55927
56 # CCD index mapping for commissioning run 2
57 _CCD_MAP_COMMISSIONING_2 = {
58 112: 106,
59 107: 105,
60 113: 107,
61 115: 109,
62 108: 110,
63 114: 108,
64 }
66 _DETECTOR_NUM_TO_UNIQUE_NAME = [
67 "1_53",
68 "1_54",
69 "1_55",
70 "1_56",
71 "1_42",
72 "1_43",
73 "1_44",
74 "1_45",
75 "1_46",
76 "1_47",
77 "1_36",
78 "1_37",
79 "1_38",
80 "1_39",
81 "1_40",
82 "1_41",
83 "0_30",
84 "0_29",
85 "0_28",
86 "1_32",
87 "1_33",
88 "1_34",
89 "0_27",
90 "0_26",
91 "0_25",
92 "0_24",
93 "1_00",
94 "1_01",
95 "1_02",
96 "1_03",
97 "0_23",
98 "0_22",
99 "0_21",
100 "0_20",
101 "1_04",
102 "1_05",
103 "1_06",
104 "1_07",
105 "0_19",
106 "0_18",
107 "0_17",
108 "0_16",
109 "1_08",
110 "1_09",
111 "1_10",
112 "1_11",
113 "0_15",
114 "0_14",
115 "0_13",
116 "0_12",
117 "1_12",
118 "1_13",
119 "1_14",
120 "1_15",
121 "0_11",
122 "0_10",
123 "0_09",
124 "0_08",
125 "1_16",
126 "1_17",
127 "1_18",
128 "1_19",
129 "0_07",
130 "0_06",
131 "0_05",
132 "0_04",
133 "1_20",
134 "1_21",
135 "1_22",
136 "1_23",
137 "0_03",
138 "0_02",
139 "0_01",
140 "0_00",
141 "1_24",
142 "1_25",
143 "1_26",
144 "1_27",
145 "0_34",
146 "0_33",
147 "0_32",
148 "1_28",
149 "1_29",
150 "1_30",
151 "0_41",
152 "0_40",
153 "0_39",
154 "0_38",
155 "0_37",
156 "0_36",
157 "0_47",
158 "0_46",
159 "0_45",
160 "0_44",
161 "0_43",
162 "0_42",
163 "0_56",
164 "0_55",
165 "0_54",
166 "0_53",
167 "0_31",
168 "1_35",
169 "0_35",
170 "1_31",
171 "1_48",
172 "1_51",
173 "1_52",
174 "1_57",
175 "0_57",
176 "0_52",
177 "0_51",
178 "0_48",
179 ]
181 @classmethod
182 def can_translate(cls, header: Mapping[str, Any], filename: str | None = None) -> bool:
183 """Indicate whether this translation class can translate the
184 supplied header.
186 There is no ``INSTRUME`` header in early HSC files, so this method
187 looks for HSC mentions in other headers. In more recent files the
188 instrument is called "Hyper Suprime-Cam".
190 Parameters
191 ----------
192 header : `dict`-like
193 Header to convert to standardized form.
194 filename : `str`, optional
195 Name of file being translated.
197 Returns
198 -------
199 can : `bool`
200 `True` if the header is recognized by this class. `False`
201 otherwise.
202 """
203 if "INSTRUME" in header:
204 return header["INSTRUME"] == "Hyper Suprime-Cam"
206 for k in ("EXP-ID", "FRAMEID"):
207 if cls.is_keyword_defined(header, k):
208 if header[k].startswith("HSC"):
209 return True
210 return False
212 @cache_translation
213 def to_exposure_id(self) -> int:
214 """Calculate unique exposure integer for this observation.
216 Returns
217 -------
218 visit : `int`
219 Integer uniquely identifying this exposure.
220 """
221 exp_id = self._header["EXP-ID"].strip()
222 m = re.search(r"^HSCE(\d{8})$", exp_id) # 2016-06-14 and new scheme
223 if m:
224 self._used_these_cards("EXP-ID")
225 return int(m.group(1))
227 # Fallback to old scheme
228 m = re.search(r"^HSC([A-Z])(\d{6})00$", exp_id)
229 if not m:
230 raise RuntimeError(f"{self._log_prefix}: Unable to interpret EXP-ID: {exp_id}")
231 letter, visit = m.groups()
232 visit = int(visit)
233 if visit == 0:
234 # Don't believe it
235 frame_id = self._header["FRAMEID"].strip()
236 m = re.search(r"^HSC([A-Z])(\d{6})\d{2}$", frame_id)
237 if not m:
238 raise RuntimeError(f"{self._log_prefix}: Unable to interpret FRAMEID: {frame_id}")
239 letter, visit = m.groups()
240 visit = int(visit)
241 if visit % 2: # Odd?
242 visit -= 1
243 self._used_these_cards("EXP-ID", "FRAMEID")
244 return visit + 1000000 * (ord(letter) - ord("A"))
246 @cache_translation
247 def to_boresight_rotation_angle(self) -> Angle:
248 # Docstring will be inherited. Property defined in properties.py
249 # Rotation angle formula determined empirically from visual inspection
250 # of HSC images. See DM-9111.
251 angle = Angle(270.0 * u.deg) - Angle(self.quantity_from_card("INST-PA", u.deg))
252 angle = angle.wrap_at("360d")
253 return angle
255 @cache_translation
256 def to_detector_num(self) -> int:
257 """Calculate the detector number.
259 Focus CCDs were numbered incorrectly in the readout software during
260 commissioning run 2. This method maps to the correct ones.
262 Returns
263 -------
264 num : `int`
265 Detector number.
266 """
267 ccd = super().to_detector_num()
268 try:
269 tjd = self._get_adjusted_mjd()
270 except Exception:
271 return ccd
273 if tjd > 390 and tjd < 405:
274 ccd = self._CCD_MAP_COMMISSIONING_2.get(ccd, ccd)
276 return ccd
278 @cache_translation
279 def to_detector_exposure_id(self) -> int:
280 # Docstring will be inherited. Property defined in properties.py
281 return self.to_exposure_id() * 200 + self.to_detector_num()
283 @cache_translation
284 def to_detector_group(self) -> str:
285 # Docstring will be inherited. Property defined in properties.py
286 unique = self.to_detector_unique_name()
287 return unique.split("_")[0]
289 @cache_translation
290 def to_detector_unique_name(self) -> str:
291 # Docstring will be inherited. Property defined in properties.py
292 # Mapping from number to unique name is defined solely in camera
293 # geom files.
294 # There is no header for it.
295 num = self.to_detector_num()
296 return self._DETECTOR_NUM_TO_UNIQUE_NAME[num]
298 @cache_translation
299 def to_detector_name(self) -> str:
300 # Docstring will be inherited. Property defined in properties.py
301 # Name is defined from unique name
302 unique = self.to_detector_unique_name()
303 return unique.split("_")[1]
305 @cache_translation
306 def to_focus_z(self) -> u.Quantity:
307 # Docstring will be inherited. Property defined in properties.py
308 foc_val = self._header["FOC-VAL"]
309 return foc_val * u.mm