Coverage for python/astro_metadata_translator/translators/hsc.py: 39%
92 statements
« prev ^ index » next coverage.py v7.2.1, created at 2023-03-13 02:40 +0000
« prev ^ index » next coverage.py v7.2.1, created at 2023-03-13 02:40 +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 typing import Any, MutableMapping, Optional
23import astropy.units as u
24from astropy.coordinates import Angle
26from ..translator import CORRECTIONS_RESOURCE_ROOT, cache_translation
27from .suprimecam import SuprimeCamTranslator
29log = logging.getLogger(__name__)
32class HscTranslator(SuprimeCamTranslator):
33 """Metadata translator for HSC standard headers."""
35 name = "HSC"
36 """Name of this translation class"""
38 supported_instrument = "HSC"
39 """Supports the HSC instrument."""
41 default_resource_root = posixpath.join(CORRECTIONS_RESOURCE_ROOT, "HSC")
42 """Default resource path root to use to locate header correction files."""
44 _const_map = {"instrument": "HSC", "boresight_rotation_coord": "sky"}
45 """Hard wire HSC even though modern headers call it Hyper Suprime-Cam"""
47 _trivial_map = {
48 "detector_serial": "T_CCDSN",
49 }
50 """One-to-one mappings"""
52 # Zero point for HSC dates: 2012-01-01 51544 -> 2000-01-01
53 _DAY0 = 55927
55 # CCD index mapping for commissioning run 2
56 _CCD_MAP_COMMISSIONING_2 = {
57 112: 106,
58 107: 105,
59 113: 107,
60 115: 109,
61 108: 110,
62 114: 108,
63 }
65 _DETECTOR_NUM_TO_UNIQUE_NAME = [
66 "1_53",
67 "1_54",
68 "1_55",
69 "1_56",
70 "1_42",
71 "1_43",
72 "1_44",
73 "1_45",
74 "1_46",
75 "1_47",
76 "1_36",
77 "1_37",
78 "1_38",
79 "1_39",
80 "1_40",
81 "1_41",
82 "0_30",
83 "0_29",
84 "0_28",
85 "1_32",
86 "1_33",
87 "1_34",
88 "0_27",
89 "0_26",
90 "0_25",
91 "0_24",
92 "1_00",
93 "1_01",
94 "1_02",
95 "1_03",
96 "0_23",
97 "0_22",
98 "0_21",
99 "0_20",
100 "1_04",
101 "1_05",
102 "1_06",
103 "1_07",
104 "0_19",
105 "0_18",
106 "0_17",
107 "0_16",
108 "1_08",
109 "1_09",
110 "1_10",
111 "1_11",
112 "0_15",
113 "0_14",
114 "0_13",
115 "0_12",
116 "1_12",
117 "1_13",
118 "1_14",
119 "1_15",
120 "0_11",
121 "0_10",
122 "0_09",
123 "0_08",
124 "1_16",
125 "1_17",
126 "1_18",
127 "1_19",
128 "0_07",
129 "0_06",
130 "0_05",
131 "0_04",
132 "1_20",
133 "1_21",
134 "1_22",
135 "1_23",
136 "0_03",
137 "0_02",
138 "0_01",
139 "0_00",
140 "1_24",
141 "1_25",
142 "1_26",
143 "1_27",
144 "0_34",
145 "0_33",
146 "0_32",
147 "1_28",
148 "1_29",
149 "1_30",
150 "0_41",
151 "0_40",
152 "0_39",
153 "0_38",
154 "0_37",
155 "0_36",
156 "0_47",
157 "0_46",
158 "0_45",
159 "0_44",
160 "0_43",
161 "0_42",
162 "0_56",
163 "0_55",
164 "0_54",
165 "0_53",
166 "0_31",
167 "1_35",
168 "0_35",
169 "1_31",
170 "1_48",
171 "1_51",
172 "1_52",
173 "1_57",
174 "0_57",
175 "0_52",
176 "0_51",
177 "0_48",
178 ]
180 @classmethod
181 def can_translate(cls, header: MutableMapping[str, Any], filename: Optional[str] = None) -> bool:
182 """Indicate whether this translation class can translate the
183 supplied header.
185 There is no ``INSTRUME`` header in early HSC files, so this method
186 looks for HSC mentions in other headers. In more recent files the
187 instrument is called "Hyper Suprime-Cam".
189 Parameters
190 ----------
191 header : `dict`-like
192 Header to convert to standardized form.
193 filename : `str`, optional
194 Name of file being translated.
196 Returns
197 -------
198 can : `bool`
199 `True` if the header is recognized by this class. `False`
200 otherwise.
201 """
202 if "INSTRUME" in header:
203 return header["INSTRUME"] == "Hyper Suprime-Cam"
205 for k in ("EXP-ID", "FRAMEID"):
206 if cls.is_keyword_defined(header, k):
207 if header[k].startswith("HSC"):
208 return True
209 return False
211 @cache_translation
212 def to_exposure_id(self) -> int:
213 """Calculate unique exposure integer for this observation
215 Returns
216 -------
217 visit : `int`
218 Integer uniquely identifying this exposure.
219 """
220 exp_id = self._header["EXP-ID"].strip()
221 m = re.search(r"^HSCE(\d{8})$", exp_id) # 2016-06-14 and new scheme
222 if m:
223 self._used_these_cards("EXP-ID")
224 return int(m.group(1))
226 # Fallback to old scheme
227 m = re.search(r"^HSC([A-Z])(\d{6})00$", exp_id)
228 if not m:
229 raise RuntimeError(f"{self._log_prefix}: Unable to interpret EXP-ID: {exp_id}")
230 letter, visit = m.groups()
231 visit = int(visit)
232 if visit == 0:
233 # Don't believe it
234 frame_id = self._header["FRAMEID"].strip()
235 m = re.search(r"^HSC([A-Z])(\d{6})\d{2}$", frame_id)
236 if not m:
237 raise RuntimeError(f"{self._log_prefix}: Unable to interpret FRAMEID: {frame_id}")
238 letter, visit = m.groups()
239 visit = int(visit)
240 if visit % 2: # Odd?
241 visit -= 1
242 self._used_these_cards("EXP-ID", "FRAMEID")
243 return visit + 1000000 * (ord(letter) - ord("A"))
245 @cache_translation
246 def to_boresight_rotation_angle(self) -> Angle:
247 # Docstring will be inherited. Property defined in properties.py
248 # Rotation angle formula determined empirically from visual inspection
249 # of HSC images. See DM-9111.
250 angle = Angle(270.0 * u.deg) - Angle(self.quantity_from_card("INST-PA", u.deg))
251 angle = angle.wrap_at("360d")
252 return angle
254 @cache_translation
255 def to_detector_num(self) -> int:
256 """Calculate the detector number.
258 Focus CCDs were numbered incorrectly in the readout software during
259 commissioning run 2. This method maps to the correct ones.
261 Returns
262 -------
263 num : `int`
264 Detector number.
265 """
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