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 currently part of obs_lsst but is written to allow it 

2# to be migrated to the astro_metadata_translator package at a later date. 

3# 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the LICENSE file in this directory for details of code ownership. 

7# 

8# Use of this source code is governed by a 3-clause BSD-style 

9# license that can be found in the LICENSE file. 

10 

11"""Metadata translation code for LSST TestStand 8 headers""" 

12 

13__all__ = ("LsstTS8Translator", ) 

14 

15import logging 

16import re 

17 

18import astropy.units as u 

19from astropy.time import Time 

20 

21from astro_metadata_translator import cache_translation 

22 

23from .lsst import LsstBaseTranslator 

24 

25log = logging.getLogger(__name__) 

26 

27 

28class LsstTS8Translator(LsstBaseTranslator): 

29 """Metadata translator for LSST Test Stand 8 data. 

30 """ 

31 

32 name = "LSST-TS8" 

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

34 

35 _const_map = { 

36 # TS8 is not attached to a telescope so many translations are null. 

37 "instrument": "LSST-TS8", 

38 "telescope": None, 

39 "location": None, 

40 "boresight_rotation_coord": None, 

41 "boresight_rotation_angle": None, 

42 "boresight_airmass": None, 

43 "tracking_radec": None, 

44 "altaz_begin": None, 

45 "object": "UNKNOWN", 

46 "relative_humidity": None, 

47 "temperature": None, 

48 "pressure": None, 

49 } 

50 

51 _trivial_map = { 

52 "science_program": "RUNNUM", 

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

54 } 

55 

56 DETECTOR_MAX = 250 

57 """Maximum number of detectors to use when calculating the 

58 detector_exposure_id.""" 

59 

60 cameraPolicyFile = "policy/ts8.yaml" 

61 

62 @classmethod 

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

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

65 supplied header. 

66 

67 There is no ``INSTRUME`` header in TS8 data. Instead we use 

68 the ``TSTAND`` header. We also look at the file name to see if 

69 it starts with "ts8-". 

70 

71 Older data has no ``TSTAND`` header so we must use a combination 

72 of headers. 

73 

74 Parameters 

75 ---------- 

76 header : `dict`-like 

77 Header to convert to standardized form. 

78 filename : `str`, optional 

79 Name of file being translated. 

80 

81 Returns 

82 ------- 

83 can : `bool` 

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

85 otherwise. 

86 """ 

87 can = cls.can_translate_with_options(header, {"TSTAND": "TS8"}, filename=filename) 

88 if can: 

89 return True 

90 

91 if "LSST_NUM" in header and "REBNAME" in header and \ 

92 "CONTNUM" in header and \ 

93 header["CONTNUM"] in ("000018910e0c", "000018ee33b7", "000018ee0f35", "000018ee3b40", 

94 "00001891fcc7", "000018edfd65", "0000123b5ba8", "000018911b05", 

95 "00001891fa3e", "000018910d7f", "000018ed9f12", "000018edf4a7", 

96 "000018ee34e6", "000018ef1464", "000018eda120", "000018edf8a2", 

97 "000018ef3819", "000018ed9486", "000018ee02c8", "000018edfb24", 

98 "000018ee34c0", "000018edfb51", "0000123b51d1", "0000123b5862", 

99 "0000123b8ca9", "0000189208fa", "0000189111af", "0000189126e1", 

100 "000018ee0618", "000018ee3b78", "000018ef1534"): 

101 return True 

102 

103 return False 

104 

105 @staticmethod 

106 def compute_exposure_id(dateobs, seqnum=0, controller=None): 

107 """Helper method to calculate the TS8 exposure_id. 

108 

109 Parameters 

110 ---------- 

111 dateobs : `str` 

112 Date of observation in FITS ISO format. 

113 seqnum : `int`, unused 

114 Sequence number. Ignored. 

115 controller : `str`, unused 

116 Controller type. Ignored. 

117 

118 Returns 

119 ------- 

120 exposure_id : `int` 

121 Exposure ID. 

122 """ 

123 # There is worry that seconds are too coarse so use 10th of second 

124 # and read the first 21 characters. 

125 exposure_id = re.sub(r"\D", "", dateobs[:21]) 

126 return int(exposure_id) 

127 

128 @cache_translation 

129 def to_datetime_begin(self): 

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

131 self._used_these_cards("MJD-OBS") 

132 return Time(self._header["MJD-OBS"], scale="utc", format="mjd") 

133 

134 @cache_translation 

135 def to_detector_name(self): 

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

137 serial = self.to_detector_serial() 

138 detector_info = self.compute_detector_info_from_serial(serial) 

139 return detector_info[1] 

140 

141 def to_detector_group(self): 

142 """Returns the name of the raft. 

143 

144 Extracted from RAFTNAME header. 

145 

146 Raftname should be of the form: 'LCA-11021_RTM-011-Dev' and 

147 the resulting name will have the form "RTM-NNN". 

148 

149 Returns 

150 ------- 

151 name : `str` 

152 Name of raft. 

153 """ 

154 raft_name = self._header["RAFTNAME"] 

155 self._used_these_cards("RAFTNAME") 

156 match = re.search(r"(RTM-\d\d\d)", raft_name) 

157 if match: 

158 return match.group(0) 

159 raise ValueError(f"RAFTNAME has unexpected form of '{raft_name}'") 

160 

161 @cache_translation 

162 def to_detector_serial(self): 

163 """Returns the serial number of the detector. 

164 

165 Returns 

166 ------- 

167 serial : `str` 

168 LSST assigned serial number. 

169 

170 Notes 

171 ----- 

172 This is the LSST assigned serial number (``LSST_NUM``), and not 

173 the manufacturer's serial number (``CCD_SERN``). 

174 """ 

175 serial = self._header["LSST_NUM"] 

176 self._used_these_cards("LSST_NUM") 

177 

178 # this seems to be appended more or less at random and should be 

179 # removed. 

180 serial = re.sub("-Dev$", "", serial) 

181 return serial 

182 

183 @cache_translation 

184 def to_physical_filter(self): 

185 """Return the filter name. 

186 

187 Uses the FILTPOS header. 

188 

189 Returns 

190 ------- 

191 filter : `str` 

192 The filter name. Returns "NONE" if no filter can be determined. 

193 

194 Notes 

195 ----- 

196 The calculations here are examples rather than being accurate. 

197 They need to be fixed once the camera acquisition system does 

198 this properly. 

199 """ 

200 

201 try: 

202 filter_pos = self._header["FILTPOS"] 

203 self._used_these_cards("FILTPOS") 

204 except KeyError: 

205 log.warning("%s: FILTPOS key not found in header (assuming NONE)", 

206 self.to_observation_id()) 

207 return "NONE" 

208 

209 try: 

210 return { 

211 2: 'g', 

212 3: 'r', 

213 4: 'i', 

214 5: 'z', 

215 6: 'y', 

216 }[filter_pos] 

217 except KeyError: 

218 log.warning("%s: Unknown filter position (assuming NONE): %d", 

219 self.to_observation_id(), filter_pos) 

220 return "NONE" 

221 

222 def to_exposure_id(self): 

223 """Generate a unique exposure ID number 

224 

225 Note that SEQNUM is not unique for a given day in TS8 data 

226 so instead we convert the ISO date of observation directly to an 

227 integer. 

228 

229 Returns 

230 ------- 

231 exposure_id : `int` 

232 Unique exposure number. 

233 """ 

234 iso = self._header["DATE-OBS"] 

235 self._used_these_cards("DATE-OBS") 

236 

237 return self.compute_exposure_id(iso) 

238 

239 # For now assume that visit IDs and exposure IDs are identical 

240 to_visit_id = to_exposure_id 

241 

242 @cache_translation 

243 def to_observation_id(self): 

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

245 filename = self._header["FILENAME"] 

246 self._used_these_cards("FILENAME") 

247 return filename[:filename.rfind(".")]