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

93 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-06 11:47 +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. 

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 # Zero point for HSC dates: 2012-01-01 51544 -> 2000-01-01 

54 _DAY0 = 55927 

55 

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 } 

65 

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 ] 

180 

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. 

185 

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

189 

190 Parameters 

191 ---------- 

192 header : `dict`-like 

193 Header to convert to standardized form. 

194 filename : `str`, optional 

195 Name of file being translated. 

196 

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" 

205 

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 

211 

212 @cache_translation 

213 def to_exposure_id(self) -> int: 

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

215 

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

226 

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

245 

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 

254 

255 @cache_translation 

256 def to_detector_num(self) -> int: 

257 """Calculate the detector number. 

258 

259 Focus CCDs were numbered incorrectly in the readout software during 

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

261 

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 

272 

273 if tjd > 390 and tjd < 405: 

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

275 

276 return ccd 

277 

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

282 

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] 

288 

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] 

297 

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] 

304 

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