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

109 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-03-28 02:59 -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 collections.abc import Mapping 

22from typing import TYPE_CHECKING, Any 

23 

24import astropy.units as u 

25from astropy.coordinates import Angle, SkyCoord 

26 

27from ..translator import CORRECTIONS_RESOURCE_ROOT, cache_translation 

28from .helpers import altaz_from_degree_headers 

29from .subaru import SubaruTranslator 

30 

31if TYPE_CHECKING: 

32 import astropy.coordinates 

33 import astropy.time 

34 

35log = logging.getLogger(__name__) 

36 

37 

38class SuprimeCamTranslator(SubaruTranslator): 

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

40 

41 name = "SuprimeCam" 

42 """Name of this translation class""" 

43 

44 supported_instrument = "SuprimeCam" 

45 """Supports the SuprimeCam instrument.""" 

46 

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

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

49 

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

51 """Constant mappings""" 

52 

53 _trivial_map: dict[str, str | list[str] | tuple[Any, ...]] = { 

54 "observation_id": "EXP-ID", 

55 "object": "OBJECT", 

56 "science_program": "PROP-ID", 

57 "detector_num": "DET-ID", 

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

59 "boresight_airmass": "AIRMASS", 

60 "relative_humidity": "OUT-HUM", 

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

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

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

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

65 } 

66 """One-to-one mappings""" 

67 

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

69 _DAY0 = 53005 

70 

71 @classmethod 

72 def can_translate(cls, header: Mapping[str, Any], filename: str | None = None) -> bool: 

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

74 supplied header. 

75 

76 Parameters 

77 ---------- 

78 header : `dict`-like 

79 Header to convert to standardized form. 

80 filename : `str`, optional 

81 Name of file being translated. 

82 

83 Returns 

84 ------- 

85 can : `bool` 

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

87 otherwise. 

88 """ 

89 if "INSTRUME" in header: 

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

91 

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

93 if cls.is_keyword_defined(header, k): 

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

95 return True 

96 return False 

97 

98 def _get_adjusted_mjd(self) -> int: 

99 """Calculate the modified julian date offset from reference day. 

100 

101 Returns 

102 ------- 

103 offset : `int` 

104 Offset day count from reference day. 

105 """ 

106 mjd = self._header["MJD"] 

107 self._used_these_cards("MJD") 

108 return int(mjd) - self._DAY0 

109 

110 @cache_translation 

111 def to_physical_filter(self) -> str: 

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

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

114 self._used_these_cards("FILTER01") 

115 # Map potential "unknown" values to standard form 

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

117 value = "unknown" 

118 elif value == "NONE": 

119 value = "empty" 

120 return value 

121 

122 @cache_translation 

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

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

125 # We know it is UTC 

126 value = self._from_fits_date_string( 

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

128 ) 

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

130 return value 

131 

132 @cache_translation 

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

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

135 # We know it is UTC 

136 value = self._from_fits_date_string( 

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

138 ) 

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

140 

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

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

143 exposure_time = self.to_exposure_time() 

144 datetime_begin = self.to_datetime_begin() 

145 exposure_end = datetime_begin + exposure_time 

146 if value < exposure_end: 

147 value = exposure_end 

148 

149 return value 

150 

151 @cache_translation 

152 def to_exposure_id(self) -> int: 

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

154 

155 Returns 

156 ------- 

157 visit : `int` 

158 Integer uniquely identifying this exposure. 

159 """ 

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

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

162 if not m: 

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

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

165 if int(exposure) == 0: 

166 # Don't believe it 

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

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

169 if not m: 

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

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

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

173 return exposure 

174 

175 @cache_translation 

176 def to_visit_id(self) -> int: 

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

178 

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

180 

181 Returns 

182 ------- 

183 exp : `int` 

184 Unique visit identifier. 

185 """ 

186 return self.to_exposure_id() 

187 

188 @cache_translation 

189 def to_observation_type(self) -> str: 

190 """Calculate the observation type. 

191 

192 Returns 

193 ------- 

194 typ : `str` 

195 Observation type. Normalized to standard set. 

196 """ 

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

198 self._used_these_cards("DATA-TYP") 

199 if obstype == "object": 

200 return "science" 

201 return obstype 

202 

203 @cache_translation 

204 def to_tracking_radec(self) -> SkyCoord: 

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

206 radec = SkyCoord( 

207 self._header["RA2000"], 

208 self._header["DEC2000"], 

209 frame="icrs", 

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

211 obstime=self.to_datetime_begin(), 

212 location=self.to_location(), 

213 ) 

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

215 return radec 

216 

217 @cache_translation 

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

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

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

221 

222 @cache_translation 

223 def to_boresight_rotation_angle(self) -> Angle: 

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

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

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

227 return angle 

228 

229 @cache_translation 

230 def to_detector_exposure_id(self) -> int: 

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

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

233 

234 @cache_translation 

235 def to_detector_name(self) -> str: 

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

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

238 num = self.to_detector_num() 

239 

240 names = ( 

241 "nausicaa", 

242 "kiki", 

243 "fio", 

244 "sophie", 

245 "sheeta", 

246 "satsuki", 

247 "chihiro", 

248 "clarisse", 

249 "ponyo", 

250 "san", 

251 ) 

252 

253 return names[num]