Hide keyboard shortcuts

Hot-keys 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

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 DECam FITS headers""" 

13 

14__all__ = ("DecamTranslator", ) 

15 

16import re 

17import posixpath 

18 

19from astropy.coordinates import EarthLocation, Angle 

20import astropy.units as u 

21 

22from ..translator import cache_translation, CORRECTIONS_RESOURCE_ROOT 

23from .fits import FitsTranslator 

24from .helpers import altaz_from_degree_headers, is_non_science, \ 

25 tracking_from_degree_headers 

26 

27 

28class DecamTranslator(FitsTranslator): 

29 """Metadata translator for DECam standard headers. 

30 """ 

31 

32 name = "DECam" 

33 """Name of this translation class""" 

34 

35 supported_instrument = "DECam" 

36 """Supports the DECam instrument.""" 

37 

38 default_resource_root = posixpath.join(CORRECTIONS_RESOURCE_ROOT, "DECam") 

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

40 

41 # DECam has no rotator, and the instrument angle on sky is set to +Y=East, 

42 # +X=South which we define as a 90 degree rotation and an X-flip. 

43 _const_map = {"boresight_rotation_angle": Angle(90*u.deg), 

44 "boresight_rotation_coord": "sky", 

45 } 

46 

47 _trivial_map = {"exposure_time": ("EXPTIME", dict(unit=u.s)), 

48 "dark_time": ("DARKTIME", dict(unit=u.s)), 

49 "boresight_airmass": ("AIRMASS", dict(checker=is_non_science)), 

50 "observation_id": "OBSID", 

51 "object": "OBJECT", 

52 "science_program": "PROPID", 

53 "detector_num": "CCDNUM", 

54 "detector_serial": "DETECTOR", 

55 "detector_unique_name": "DETPOS", 

56 "telescope": ("TELESCOP", dict(default="CTIO 4.0-m telescope")), 

57 "instrument": ("INSTRUME", dict(default="DECam")), 

58 # Ensure that reasonable values are always available 

59 "relative_humidity": ("HUMIDITY", dict(default=40., minimum=0, maximum=100.)), 

60 "temperature": ("OUTTEMP", dict(unit=u.deg_C, default=10., minimum=-10., maximum=40.)), 

61 # Header says torr but seems to be mbar. Use hPa unit 

62 # which is the SI equivalent of mbar. 

63 "pressure": ("PRESSURE", dict(unit=u.hPa, 

64 default=771.611, minimum=700., maximum=850.)), 

65 } 

66 

67 # Unique detector names are currently not used but are read directly from 

68 # header. 

69 # The detector_group could be N or S with detector_name corresponding 

70 # to the number in that group. 

71 detector_names = { 

72 1: 'S29', 2: 'S30', 3: 'S31', 4: 'S25', 5: 'S26', 6: 'S27', 7: 'S28', 8: 'S20', 9: 'S21', 

73 10: 'S22', 11: 'S23', 12: 'S24', 13: 'S14', 14: 'S15', 15: 'S16', 16: 'S17', 17: 'S18', 

74 18: 'S19', 19: 'S8', 20: 'S9', 21: 'S10', 22: 'S11', 23: 'S12', 24: 'S13', 25: 'S1', 26: 'S2', 

75 27: 'S3', 28: 'S4', 29: 'S5', 30: 'S6', 31: 'S7', 32: 'N1', 33: 'N2', 34: 'N3', 35: 'N4', 

76 36: 'N5', 37: 'N6', 38: 'N7', 39: 'N8', 40: 'N9', 41: 'N10', 42: 'N11', 43: 'N12', 44: 'N13', 

77 45: 'N14', 46: 'N15', 47: 'N16', 48: 'N17', 49: 'N18', 50: 'N19', 51: 'N20', 52: 'N21', 

78 53: 'N22', 54: 'N23', 55: 'N24', 56: 'N25', 57: 'N26', 58: 'N27', 59: 'N28', 60: 'N29', 

79 62: 'N31'} 

80 

81 @classmethod 

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

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

84 supplied header. 

85 

86 Checks the INSTRUME and FILTER headers. 

87 

88 Parameters 

89 ---------- 

90 header : `dict`-like 

91 Header to convert to standardized form. 

92 filename : `str`, optional 

93 Name of file being translated. 

94 

95 Returns 

96 ------- 

97 can : `bool` 

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

99 otherwise. 

100 """ 

101 # Use INSTRUME. Because of defaulting behavior only do this 

102 # if we really have an INSTRUME header 

103 if "INSTRUME" in header: 

104 via_instrume = super().can_translate(header, filename=filename) 

105 if via_instrume: 

106 return via_instrume 

107 if cls.is_keyword_defined(header, "FILTER") and "DECam" in header["FILTER"]: 

108 return True 

109 return False 

110 

111 @cache_translation 

112 def to_exposure_id(self): 

113 """Calculate exposure ID. 

114 

115 Returns 

116 ------- 

117 id : `int` 

118 ID of exposure. 

119 """ 

120 value = self._header["EXPNUM"] 

121 self._used_these_cards("EXPNUM") 

122 return value 

123 

124 @cache_translation 

125 def to_observation_counter(self): 

126 """Return the lifetime exposure number. 

127 

128 Returns 

129 ------- 

130 sequence : `int` 

131 The observation counter. 

132 """ 

133 return self.to_exposure_id() 

134 

135 @cache_translation 

136 def to_visit_id(self): 

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

138 return self.to_exposure_id() 

139 

140 @cache_translation 

141 def to_datetime_end(self): 

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

143 # Instcals have no DATE-END or DTUTC 

144 datetime_end = self._from_fits_date("DTUTC", scale="utc") 

145 if datetime_end is None: 

146 datetime_end = self.to_datetime_begin() + self.to_exposure_time() 

147 return datetime_end 

148 

149 def _translate_from_calib_id(self, field): 

150 """Fetch the ID from the CALIB_ID header. 

151 

152 Calibration products made with constructCalibs have some metadata 

153 saved in its FITS header CALIB_ID. 

154 """ 

155 data = self._header["CALIB_ID"] 

156 match = re.search(r".*%s=(\S+)" % field, data) 

157 self._used_these_cards("CALIB_ID") 

158 return match.groups()[0] 

159 

160 @cache_translation 

161 def to_physical_filter(self): 

162 """Calculate physical filter. 

163 

164 Return `None` if the keyword FILTER does not exist in the header, 

165 which can happen for some valid Community Pipeline products. 

166 

167 Returns 

168 ------- 

169 filter : `str` 

170 The full filter name. 

171 """ 

172 if self.is_key_ok("FILTER"): 

173 value = self._header["FILTER"].strip() 

174 self._used_these_cards("FILTER") 

175 return value 

176 elif self.is_key_ok("CALIB_ID"): 

177 return self._translate_from_calib_id("filter") 

178 else: 

179 return None 

180 

181 @cache_translation 

182 def to_location(self): 

183 """Calculate the observatory location. 

184 

185 Returns 

186 ------- 

187 location : `astropy.coordinates.EarthLocation` 

188 An object representing the location of the telescope. 

189 """ 

190 

191 if self.is_key_ok("OBS-LONG"): 

192 # OBS-LONG has west-positive sign so must be flipped 

193 lon = self._header["OBS-LONG"] * -1.0 

194 value = EarthLocation.from_geodetic(lon, self._header["OBS-LAT"], self._header["OBS-ELEV"]) 

195 self._used_these_cards("OBS-LONG", "OBS-LAT", "OBS-ELEV") 

196 else: 

197 # Look up the value since some files do not have location 

198 value = EarthLocation.of_site("ctio") 

199 

200 return value 

201 

202 @cache_translation 

203 def to_observation_type(self): 

204 """Calculate the observation type. 

205 

206 Returns 

207 ------- 

208 typ : `str` 

209 Observation type. Normalized to standard set. 

210 """ 

211 if not self.is_key_ok("OBSTYPE"): 

212 return "none" 

213 obstype = self._header["OBSTYPE"].strip().lower() 

214 self._used_these_cards("OBSTYPE") 

215 if obstype == "object": 

216 return "science" 

217 return obstype 

218 

219 @cache_translation 

220 def to_tracking_radec(self): 

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

222 radecsys = ("RADESYS",) 

223 radecpairs = (("TELRA", "TELDEC"),) 

224 return tracking_from_degree_headers(self, radecsys, radecpairs, unit=(u.hourangle, u.deg)) 

225 

226 @cache_translation 

227 def to_altaz_begin(self): 

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

229 return altaz_from_degree_headers(self, (("ZD", "AZ"),), 

230 self.to_datetime_begin(), is_zd=set(["ZD"])) 

231 

232 @cache_translation 

233 def to_detector_exposure_id(self): 

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

235 exposure_id = self.to_exposure_id() 

236 if exposure_id is None: 

237 return None 

238 return int("{:07d}{:02d}".format(exposure_id, self.to_detector_num())) 

239 

240 @cache_translation 

241 def to_detector_group(self): 

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

243 name = self.to_detector_unique_name() 

244 return name[0] 

245 

246 @cache_translation 

247 def to_detector_name(self): 

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

249 name = self.to_detector_unique_name() 

250 return name[1:]