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

92 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-08-18 18:35 +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 typing import Any, MutableMapping, Optional 

22 

23import astropy.units as u 

24from astropy.coordinates import Angle 

25 

26from ..translator import CORRECTIONS_RESOURCE_ROOT, cache_translation 

27from .suprimecam import SuprimeCamTranslator 

28 

29log = logging.getLogger(__name__) 

30 

31 

32class HscTranslator(SuprimeCamTranslator): 

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

34 

35 name = "HSC" 

36 """Name of this translation class""" 

37 

38 supported_instrument = "HSC" 

39 """Supports the HSC instrument.""" 

40 

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

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

43 

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

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

46 

47 _trivial_map = { 

48 "detector_serial": "T_CCDSN", 

49 } 

50 """One-to-one mappings""" 

51 

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

53 _DAY0 = 55927 

54 

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 } 

64 

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 ] 

179 

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. 

184 

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

188 

189 Parameters 

190 ---------- 

191 header : `dict`-like 

192 Header to convert to standardized form. 

193 filename : `str`, optional 

194 Name of file being translated. 

195 

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" 

204 

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 

210 

211 @cache_translation 

212 def to_exposure_id(self) -> int: 

213 """Calculate unique exposure integer for this observation 

214 

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

225 

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

244 

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 

253 

254 @cache_translation 

255 def to_detector_num(self) -> int: 

256 """Calculate the detector number. 

257 

258 Focus CCDs were numbered incorrectly in the readout software during 

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

260 

261 Returns 

262 ------- 

263 num : `int` 

264 Detector number. 

265 """ 

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