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 UCDavis Test Stand headers""" 

12 

13__all__ = ("LsstUCDCamTranslator", ) 

14 

15import logging 

16import re 

17import os.path 

18 

19import astropy.units as u 

20from astropy.time import Time 

21 

22from astro_metadata_translator import cache_translation 

23 

24from .lsst import LsstBaseTranslator 

25 

26log = logging.getLogger(__name__) 

27 

28# There is only one detector name used 

29_DETECTOR_NAME = "S00" 

30 

31 

32class LsstUCDCamTranslator(LsstBaseTranslator): 

33 """Metadata translator for LSST UC Davis test camera data. 

34 

35 This instrument is a test system for individual LSST CCDs. 

36 To fit this instrument into the standard naming convention for LSST 

37 instruments we use a fixed detector name (S00) and assign a different 

38 raft name to each detector. The detector number changes for each CCD. 

39 """ 

40 

41 name = "LSST-UCDCam" 

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

43 

44 _const_map = { 

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

46 "telescope": "LSST", 

47 "location": None, 

48 "boresight_rotation_coord": None, 

49 "boresight_rotation_angle": None, 

50 "boresight_airmass": None, 

51 "tracking_radec": None, 

52 "altaz_begin": None, 

53 "object": "UNKNOWN", 

54 "relative_humidity": None, 

55 "temperature": None, 

56 "pressure": None, 

57 "detector_name": _DETECTOR_NAME, 

58 } 

59 

60 _trivial_map = { 

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

62 "detector_serial": "LSST_NUM", 

63 } 

64 

65 DETECTOR_NAME = _DETECTOR_NAME 

66 """Fixed name of single sensor in raft.""" 

67 

68 _detector_map = { 

69 "E2V-CCD250-112-04": (0, "R00"), 

70 "ITL-3800C-029": (1, "R01"), 

71 "ITL-3800C-002": (2, "R02"), 

72 "E2V-CCD250-112-09": (0, "R03"), 

73 } 

74 """Map detector serial to raft and detector number. Eventually the 

75 detector number will come out of the policy camera definition.""" 

76 

77 DETECTOR_MAX = 3 

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

79 detector_exposure_id.""" 

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 Parameters 

87 ---------- 

88 header : `dict`-like 

89 Header to convert to standardized form. 

90 filename : `str`, optional 

91 Name of file being translated. 

92 

93 Returns 

94 ------- 

95 can : `bool` 

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

97 otherwise. 

98 """ 

99 # Check 3 headers that all have to match 

100 for k, v in (("ORIGIN", "UCDAVIS"), ("INSTRUME", "SAO"), ("TSTAND", "LSST_OPTICAL_SIMULATOR")): 

101 if k not in header: 

102 return False 

103 if header[k] != v: 

104 return False 

105 return True 

106 

107 @classmethod 

108 def compute_detector_num_from_name(cls, detector_group, detector_name): 

109 """Helper method to return the detector number from the name. 

110 

111 Parameters 

112 ---------- 

113 detector_group : `str` 

114 Detector group name. This is generally the raft name. 

115 detector_name : `str` 

116 Detector name. Checked to ensure it is the expected name. 

117 

118 Returns 

119 ------- 

120 num : `int` 

121 Detector number. 

122 

123 Raises 

124 ------ 

125 ValueError 

126 The supplied name is not known. 

127 """ 

128 if detector_name != cls.DETECTOR_NAME: 

129 raise ValueError(f"Detector {detector_name} is not known to UCDCam") 

130 for num, group in cls._detector_map.values(): 

131 if group == detector_group: 

132 return num 

133 raise ValueError(f"Detector {detector_group}_{detector_name} is not known to UCDCam") 

134 

135 @classmethod 

136 def compute_detector_group_from_num(cls, detector_num): 

137 """Helper method to return the detector group from the number. 

138 

139 Parameters 

140 ---------- 

141 detector_num : `int` 

142 Detector number. 

143 

144 Returns 

145 ------- 

146 group : `str` 

147 Detector group. 

148 

149 Raises 

150 ------ 

151 ValueError 

152 The supplied number is not known. 

153 """ 

154 for num, group in cls._detector_map.values(): 

155 if num == detector_num: 

156 return group 

157 raise ValueError(f"Detector {detector_num} is not known for UCDCam") 

158 

159 @staticmethod 

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

161 """Helper method to calculate the exposure_id. 

162 

163 Parameters 

164 ---------- 

165 dateobs : `str` 

166 Date of observation in FITS ISO format. 

167 seqnum : `int`, unused 

168 Sequence number. Ignored. 

169 controller : `str`, unused 

170 Controller type. Ignored. 

171 

172 Returns 

173 ------- 

174 exposure_id : `int` 

175 Exposure ID. 

176 """ 

177 # Use 1 second resolution 

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

179 return int(exposure_id) 

180 

181 @cache_translation 

182 def to_instrument(self): 

183 """Calculate the instrument name. 

184 

185 Returns 

186 ------- 

187 instrume : `str` 

188 Name of the test stand. We do not distinguish between ITL and 

189 E2V. 

190 """ 

191 return self.name 

192 

193 @cache_translation 

194 def to_datetime_begin(self): 

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

196 self._used_these_cards("MJD") 

197 mjd = float(self._header["MJD"]) # Header uses a FITS string 

198 return Time(mjd, scale="utc", format="mjd") 

199 

200 @cache_translation 

201 def to_detector_num(self): 

202 """Determine the number associated with this detector. 

203 

204 Returns 

205 ------- 

206 num : `int` 

207 The number of the detector. Each CCD gets a different number. 

208 """ 

209 serial = self.to_detector_serial() 

210 return self._detector_map[serial][0] 

211 

212 @cache_translation 

213 def to_detector_group(self): 

214 """Determine the pseudo raft name associated with this detector. 

215 

216 Returns 

217 ------- 

218 raft : `str` 

219 The name of the raft. The raft is derived from the serial number 

220 of the detector. 

221 """ 

222 serial = self.to_detector_serial() 

223 return self._detector_map[serial][1] 

224 

225 @cache_translation 

226 def to_physical_filter(self): 

227 """Return the filter name. 

228 

229 Uses the FILTER header. 

230 

231 Returns 

232 ------- 

233 filter : `str` 

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

235 """ 

236 

237 if "FILTER" in self._header: 

238 self._used_these_cards("FILTER") 

239 return self._header["FILTER"].lower() 

240 else: 

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

242 self.to_observation_id()) 

243 return "NONE" 

244 

245 def to_exposure_id(self): 

246 """Generate a unique exposure ID number 

247 

248 Note that SEQNUM is not unique for a given day 

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

250 integer. 

251 

252 Returns 

253 ------- 

254 exposure_id : `int` 

255 Unique exposure number. 

256 """ 

257 date = self.to_datetime_begin() 

258 return self.compute_exposure_id(date.isot) 

259 

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

261 to_visit_id = to_exposure_id 

262 

263 @cache_translation 

264 def to_observation_id(self): 

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

266 filename = self._header["FILENAME"] 

267 self._used_these_cards("FILENAME") 

268 return os.path.splitext(os.path.basename(filename))[0] 

269 

270 @cache_translation 

271 def to_science_program(self): 

272 """Calculate the run number for this observation. 

273 

274 There is no explicit run header, so instead treat each day 

275 as the run in YYYY-MM-DD format. 

276 

277 Returns 

278 ------- 

279 run : `str` 

280 YYYY-MM-DD string corresponding to the date of observation. 

281 """ 

282 # Get a copy so that we can edit the default formatting 

283 date = self.to_datetime_begin().copy() 

284 date.format = "iso" 

285 date.out_subfmt = "date" # YYYY-MM-DD format 

286 return str(date)