Coverage for python/astro_metadata_translator/translators/suprimecam.py: 39%

111 statements  

« prev     ^ index     » next       coverage.py v7.2.1, created at 2023-03-12 20:36 -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 SuprimeCam FITS headers""" 

13 

14from __future__ import annotations 

15 

16__all__ = ("SuprimeCamTranslator",) 

17 

18import logging 

19import posixpath 

20import re 

21from typing import TYPE_CHECKING, Any, Dict, List, MutableMapping, Optional, Tuple, Union 

22 

23import astropy.units as u 

24from astropy.coordinates import Angle, SkyCoord 

25 

26from ..translator import CORRECTIONS_RESOURCE_ROOT, cache_translation 

27from .helpers import altaz_from_degree_headers 

28from .subaru import SubaruTranslator 

29 

30if TYPE_CHECKING: 30 ↛ 31line 30 didn't jump to line 31, because the condition on line 30 was never true

31 import astropy.coordinates 

32 import astropy.time 

33 

34log = logging.getLogger(__name__) 

35 

36 

37class SuprimeCamTranslator(SubaruTranslator): 

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

39 

40 name = "SuprimeCam" 

41 """Name of this translation class""" 

42 

43 supported_instrument = "SuprimeCam" 

44 """Supports the SuprimeCam instrument.""" 

45 

46 default_resource_root = posixpath.join(CORRECTIONS_RESOURCE_ROOT, "SuprimeCam") 

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

48 

49 _const_map = {"boresight_rotation_coord": "unknown", "detector_group": None} 

50 """Constant mappings""" 

51 

52 _trivial_map: Dict[str, Union[str, List[str], Tuple[Any, ...]]] = { 

53 "observation_id": "EXP-ID", 

54 "object": "OBJECT", 

55 "science_program": "PROP-ID", 

56 "detector_num": "DET-ID", 

57 "detector_serial": "DETECTOR", # DETECTOR is the "call name" 

58 "boresight_airmass": "AIRMASS", 

59 "relative_humidity": "OUT-HUM", 

60 "temperature": ("OUT-TMP", dict(unit=u.K)), 

61 "pressure": ("OUT-PRS", dict(unit=u.hPa)), 

62 "exposure_time": ("EXPTIME", dict(unit=u.s)), 

63 "dark_time": ("EXPTIME", dict(unit=u.s)), # Assume same as exposure time 

64 } 

65 """One-to-one mappings""" 

66 

67 # Zero point for SuprimeCam dates: 2004-01-01 

68 _DAY0 = 53005 

69 

70 @classmethod 

71 def can_translate(cls, header: MutableMapping[str, Any], filename: Optional[str] = None) -> bool: 

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

73 supplied header. 

74 

75 Parameters 

76 ---------- 

77 header : `dict`-like 

78 Header to convert to standardized form. 

79 filename : `str`, optional 

80 Name of file being translated. 

81 

82 Returns 

83 ------- 

84 can : `bool` 

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

86 otherwise. 

87 """ 

88 if "INSTRUME" in header: 

89 return header["INSTRUME"] == "SuprimeCam" 

90 

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

92 if cls.is_keyword_defined(header, k): 

93 if header[k].startswith("SUP"): 

94 return True 

95 return False 

96 

97 def _get_adjusted_mjd(self) -> int: 

98 """Calculate the modified julian date offset from reference day 

99 

100 Returns 

101 ------- 

102 offset : `int` 

103 Offset day count from reference day. 

104 """ 

105 mjd = self._header["MJD"] 

106 self._used_these_cards("MJD") 

107 return int(mjd) - self._DAY0 

108 

109 @cache_translation 

110 def to_physical_filter(self) -> str: 

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

112 value = self._header["FILTER01"].strip().upper() 

113 self._used_these_cards("FILTER01") 

114 # Map potential "unknown" values to standard form 

115 if value in {"UNRECOGNIZED", "UNRECOGNISED", "NOTSET", "UNKNOWN"}: 

116 value = "unknown" 

117 elif value == "NONE": 

118 value = "empty" 

119 return value 

120 

121 @cache_translation 

122 def to_datetime_begin(self) -> astropy.time.Time: 

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

124 # We know it is UTC 

125 value = self._from_fits_date_string( 

126 self._header["DATE-OBS"], time_str=self._header["UT-STR"], scale="utc" 

127 ) 

128 self._used_these_cards("DATE-OBS", "UT-STR") 

129 return value 

130 

131 @cache_translation 

132 def to_datetime_end(self) -> astropy.time.Time: 

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

134 # We know it is UTC 

135 value = self._from_fits_date_string( 

136 self._header["DATE-OBS"], time_str=self._header["UT-END"], scale="utc" 

137 ) 

138 self._used_these_cards("DATE-OBS", "UT-END") 

139 

140 # Sometimes the end time is less than the begin time plus the 

141 # exposure time so we have to check for that. 

142 exposure_time = self.to_exposure_time() 

143 datetime_begin = self.to_datetime_begin() 

144 exposure_end = datetime_begin + exposure_time 

145 if value < exposure_end: 

146 value = exposure_end 

147 

148 return value 

149 

150 @cache_translation 

151 def to_exposure_id(self) -> int: 

152 """Calculate unique exposure integer for this observation 

153 

154 Returns 

155 ------- 

156 visit : `int` 

157 Integer uniquely identifying this exposure. 

158 """ 

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

160 m = re.search(r"^SUP[A-Z](\d{7})0$", exp_id) 

161 if not m: 

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

163 exposure = int(m.group(1)) 

164 if int(exposure) == 0: 

165 # Don't believe it 

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

167 m = re.search(r"^SUP[A-Z](\d{7})\d{1}$", frame_id) 

168 if not m: 

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

170 exposure = int(m.group(1)) 

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

172 return exposure 

173 

174 @cache_translation 

175 def to_visit_id(self) -> int: 

176 """Calculate the unique integer ID for this visit. 

177 

178 Assumed to be identical to the exposure ID in this implementation. 

179 

180 Returns 

181 ------- 

182 exp : `int` 

183 Unique visit identifier. 

184 """ 

185 return self.to_exposure_id() 

186 

187 @cache_translation 

188 def to_observation_type(self) -> str: 

189 """Calculate the observation type. 

190 

191 Returns 

192 ------- 

193 typ : `str` 

194 Observation type. Normalized to standard set. 

195 """ 

196 obstype = self._header["DATA-TYP"].strip().lower() 

197 self._used_these_cards("DATA-TYP") 

198 if obstype == "object": 

199 return "science" 

200 return obstype 

201 

202 @cache_translation 

203 def to_tracking_radec(self) -> SkyCoord: 

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

205 radec = SkyCoord( 

206 self._header["RA2000"], 

207 self._header["DEC2000"], 

208 frame="icrs", 

209 unit=(u.hourangle, u.deg), 

210 obstime=self.to_datetime_begin(), 

211 location=self.to_location(), 

212 ) 

213 self._used_these_cards("RA2000", "DEC2000") 

214 return radec 

215 

216 @cache_translation 

217 def to_altaz_begin(self) -> astropy.coordinates.AltAz: 

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

219 return altaz_from_degree_headers(self, (("ALTITUDE", "AZIMUTH"),), self.to_datetime_begin()) 

220 

221 @cache_translation 

222 def to_boresight_rotation_angle(self) -> Angle: 

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

224 angle = Angle(self.quantity_from_card("INR-STR", u.deg)) 

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

226 return angle 

227 

228 @cache_translation 

229 def to_detector_exposure_id(self) -> int: 

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

231 return self.to_exposure_id() * 10 + self.to_detector_num() 

232 

233 @cache_translation 

234 def to_detector_name(self) -> str: 

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

236 # See https://subarutelescope.org/Observing/Instruments/SCam/ccd.html 

237 num = self.to_detector_num() 

238 

239 names = ( 

240 "nausicaa", 

241 "kiki", 

242 "fio", 

243 "sophie", 

244 "sheeta", 

245 "satsuki", 

246 "chihiro", 

247 "clarisse", 

248 "ponyo", 

249 "san", 

250 ) 

251 

252 return names[num]