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

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

81 statements  

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 

14__all__ = ("HscTranslator", ) 

15 

16import re 

17import logging 

18import posixpath 

19 

20import astropy.units as u 

21from astropy.coordinates import Angle 

22 

23from ..translator import cache_translation, CORRECTIONS_RESOURCE_ROOT 

24from .suprimecam import SuprimeCamTranslator 

25 

26log = logging.getLogger(__name__) 

27 

28 

29class HscTranslator(SuprimeCamTranslator): 

30 """Metadata translator for HSC standard headers. 

31 """ 

32 

33 name = "HSC" 

34 """Name of this translation class""" 

35 

36 supported_instrument = "HSC" 

37 """Supports the HSC instrument.""" 

38 

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

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

41 

42 _const_map = {"instrument": "HSC", 

43 "boresight_rotation_coord": "sky"} 

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

45 

46 _trivial_map = {"detector_serial": "T_CCDSN", 

47 } 

48 """One-to-one mappings""" 

49 

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

51 _DAY0 = 55927 

52 

53 # CCD index mapping for commissioning run 2 

54 _CCD_MAP_COMMISSIONING_2 = {112: 106, 

55 107: 105, 

56 113: 107, 

57 115: 109, 

58 108: 110, 

59 114: 108, 

60 } 

61 

62 _DETECTOR_NUM_TO_UNIQUE_NAME = [ 

63 '1_53', 

64 '1_54', 

65 '1_55', 

66 '1_56', 

67 '1_42', 

68 '1_43', 

69 '1_44', 

70 '1_45', 

71 '1_46', 

72 '1_47', 

73 '1_36', 

74 '1_37', 

75 '1_38', 

76 '1_39', 

77 '1_40', 

78 '1_41', 

79 '0_30', 

80 '0_29', 

81 '0_28', 

82 '1_32', 

83 '1_33', 

84 '1_34', 

85 '0_27', 

86 '0_26', 

87 '0_25', 

88 '0_24', 

89 '1_00', 

90 '1_01', 

91 '1_02', 

92 '1_03', 

93 '0_23', 

94 '0_22', 

95 '0_21', 

96 '0_20', 

97 '1_04', 

98 '1_05', 

99 '1_06', 

100 '1_07', 

101 '0_19', 

102 '0_18', 

103 '0_17', 

104 '0_16', 

105 '1_08', 

106 '1_09', 

107 '1_10', 

108 '1_11', 

109 '0_15', 

110 '0_14', 

111 '0_13', 

112 '0_12', 

113 '1_12', 

114 '1_13', 

115 '1_14', 

116 '1_15', 

117 '0_11', 

118 '0_10', 

119 '0_09', 

120 '0_08', 

121 '1_16', 

122 '1_17', 

123 '1_18', 

124 '1_19', 

125 '0_07', 

126 '0_06', 

127 '0_05', 

128 '0_04', 

129 '1_20', 

130 '1_21', 

131 '1_22', 

132 '1_23', 

133 '0_03', 

134 '0_02', 

135 '0_01', 

136 '0_00', 

137 '1_24', 

138 '1_25', 

139 '1_26', 

140 '1_27', 

141 '0_34', 

142 '0_33', 

143 '0_32', 

144 '1_28', 

145 '1_29', 

146 '1_30', 

147 '0_41', 

148 '0_40', 

149 '0_39', 

150 '0_38', 

151 '0_37', 

152 '0_36', 

153 '0_47', 

154 '0_46', 

155 '0_45', 

156 '0_44', 

157 '0_43', 

158 '0_42', 

159 '0_56', 

160 '0_55', 

161 '0_54', 

162 '0_53', 

163 '0_31', 

164 '1_35', 

165 '0_35', 

166 '1_31', 

167 '1_48', 

168 '1_51', 

169 '1_52', 

170 '1_57', 

171 '0_57', 

172 '0_52', 

173 '0_51', 

174 '0_48', 

175 ] 

176 

177 @classmethod 

178 def can_translate(cls, header, filename=None): 

179 """Indicate whether this translation class can translate the 

180 supplied header. 

181 

182 There is no ``INSTRUME`` header in early HSC files, so this method 

183 looks for HSC mentions in other headers. In more recent files the 

184 instrument is called "Hyper Suprime-Cam". 

185 

186 Parameters 

187 ---------- 

188 header : `dict`-like 

189 Header to convert to standardized form. 

190 filename : `str`, optional 

191 Name of file being translated. 

192 

193 Returns 

194 ------- 

195 can : `bool` 

196 `True` if the header is recognized by this class. `False` 

197 otherwise. 

198 """ 

199 if "INSTRUME" in header: 

200 return header["INSTRUME"] == "Hyper Suprime-Cam" 

201 

202 for k in ("EXP-ID", "FRAMEID"): 

203 if cls.is_keyword_defined(header, k): 

204 if header[k].startswith("HSC"): 

205 return True 

206 return False 

207 

208 @cache_translation 

209 def to_exposure_id(self): 

210 """Calculate unique exposure integer for this observation 

211 

212 Returns 

213 ------- 

214 visit : `int` 

215 Integer uniquely identifying this exposure. 

216 """ 

217 exp_id = self._header["EXP-ID"].strip() 

218 m = re.search(r"^HSCE(\d{8})$", exp_id) # 2016-06-14 and new scheme 

219 if m: 

220 self._used_these_cards("EXP-ID") 

221 return int(m.group(1)) 

222 

223 # Fallback to old scheme 

224 m = re.search(r"^HSC([A-Z])(\d{6})00$", exp_id) 

225 if not m: 

226 raise RuntimeError(f"{self._log_prefix}: Unable to interpret EXP-ID: {exp_id}") 

227 letter, visit = m.groups() 

228 visit = int(visit) 

229 if visit == 0: 

230 # Don't believe it 

231 frame_id = self._header["FRAMEID"].strip() 

232 m = re.search(r"^HSC([A-Z])(\d{6})\d{2}$", frame_id) 

233 if not m: 

234 raise RuntimeError(f"{self._log_prefix}: Unable to interpret FRAMEID: {frame_id}") 

235 letter, visit = m.groups() 

236 visit = int(visit) 

237 if visit % 2: # Odd? 

238 visit -= 1 

239 self._used_these_cards("EXP-ID", "FRAMEID") 

240 return visit + 1000000*(ord(letter) - ord("A")) 

241 

242 @cache_translation 

243 def to_boresight_rotation_angle(self): 

244 # Docstring will be inherited. Property defined in properties.py 

245 # Rotation angle formula determined empirically from visual inspection 

246 # of HSC images. See DM-9111. 

247 angle = Angle(270.*u.deg) - Angle(self.quantity_from_card("INST-PA", u.deg)) 

248 angle = angle.wrap_at("360d") 

249 return angle 

250 

251 @cache_translation 

252 def to_detector_num(self): 

253 """Calculate the detector number. 

254 

255 Focus CCDs were numbered incorrectly in the readout software during 

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

257 

258 Returns 

259 ------- 

260 num : `int` 

261 Detector number. 

262 """ 

263 

264 ccd = super().to_detector_num() 

265 try: 

266 tjd = self._get_adjusted_mjd() 

267 except Exception: 

268 return ccd 

269 

270 if tjd > 390 and tjd < 405: 

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

272 

273 return ccd 

274 

275 @cache_translation 

276 def to_detector_exposure_id(self): 

277 # Docstring will be inherited. Property defined in properties.py 

278 return self.to_exposure_id() * 200 + self.to_detector_num() 

279 

280 @cache_translation 

281 def to_detector_group(self): 

282 # Docstring will be inherited. Property defined in properties.py 

283 unique = self.to_detector_unique_name() 

284 return unique.split("_")[0] 

285 

286 @cache_translation 

287 def to_detector_unique_name(self): 

288 # Docstring will be inherited. Property defined in properties.py 

289 # Mapping from number to unique name is defined solely in camera 

290 # geom files. 

291 # There is no header for it. 

292 num = self.to_detector_num() 

293 return self._DETECTOR_NUM_TO_UNIQUE_NAME[num] 

294 

295 @cache_translation 

296 def to_detector_name(self): 

297 # Docstring will be inherited. Property defined in properties.py 

298 # Name is defined from unique name 

299 unique = self.to_detector_unique_name() 

300 return unique.split("_")[1]