Coverage for python/astro_metadata_translator/translators/hsc.py: 49%

95 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-06 03:48 -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. 

11 

12"""Metadata translation code for HSC FITS headers.""" 

13 

14from __future__ import annotations 

15 

16__all__ = ("HscTranslator",) 

17 

18import logging 

19import posixpath 

20import re 

21from collections.abc import Mapping 

22from typing import Any 

23 

24import astropy.units as u 

25from astropy.coordinates import Angle 

26 

27from ..translator import CORRECTIONS_RESOURCE_ROOT, cache_translation 

28from .suprimecam import SuprimeCamTranslator 

29 

30log = logging.getLogger(__name__) 

31 

32 

33class HscTranslator(SuprimeCamTranslator): 

34 """Metadata translator for HSC standard headers.""" 

35 

36 name = "HSC" 

37 """Name of this translation class""" 

38 

39 supported_instrument = "HSC" 

40 """Supports the HSC instrument.""" 

41 

42 default_resource_root = posixpath.join(CORRECTIONS_RESOURCE_ROOT, "HSC") 

43 """Default resource path root to use to locate header correction files.""" 

44 

45 _const_map = {"instrument": "HSC", "boresight_rotation_coord": "sky"} 

46 """Hard wire HSC even though modern headers call it Hyper Suprime-Cam""" 

47 

48 _trivial_map = { 

49 "detector_serial": "T_CCDSN", 

50 } 

51 """One-to-one mappings""" 

52 

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") 

64 

65 # Zero point for HSC dates: 2012-01-01 51544 -> 2000-01-01 

66 _DAY0 = 55927 

67 

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 } 

77 

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 ] 

192 

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. 

197 

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". 

201 

202 Parameters 

203 ---------- 

204 header : `dict`-like 

205 Header to convert to standardized form. 

206 filename : `str`, optional 

207 Name of file being translated. 

208 

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" 

217 

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 

223 

224 @cache_translation 

225 def to_exposure_id(self) -> int: 

226 """Calculate unique exposure integer for this observation. 

227 

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)) 

238 

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")) 

257 

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 

266 

267 @cache_translation 

268 def to_detector_num(self) -> int: 

269 """Calculate the detector number. 

270 

271 Focus CCDs were numbered incorrectly in the readout software during 

272 commissioning run 2. This method maps to the correct ones. 

273 

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 

284 

285 if tjd > 390 and tjd < 405: 

286 ccd = self._CCD_MAP_COMMISSIONING_2.get(ccd, ccd) 

287 

288 return ccd 

289 

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() 

294 

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] 

300 

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] 

309 

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] 

316 

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