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, TimeDelta 

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 # UCDCam is not attached to a telescope so many translations are null. 

46 "instrument": "LSST-UCDCam", 

47 "telescope": None, 

48 "location": None, 

49 "boresight_rotation_coord": None, 

50 "boresight_rotation_angle": None, 

51 "boresight_airmass": None, 

52 "tracking_radec": None, 

53 "altaz_begin": None, 

54 "object": "UNKNOWN", 

55 "relative_humidity": None, 

56 "temperature": None, 

57 "pressure": None, 

58 "detector_name": _DETECTOR_NAME, 

59 } 

60 

61 _trivial_map = { 

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

63 "detector_serial": "LSST_NUM", 

64 } 

65 

66 DETECTOR_NAME = _DETECTOR_NAME 

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

68 

69 _detector_map = { 

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

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

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

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

74 } 

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

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

77 

78 DETECTOR_MAX = 3 

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

80 detector_exposure_id.""" 

81 

82 _ROLLOVER_TIME = TimeDelta(8*60*60, scale="tai", format="sec") 

83 """Time delta for the definition of a Rubin Test Stand start of day.""" 

84 

85 @classmethod 

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

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

88 supplied header. 

89 

90 Parameters 

91 ---------- 

92 header : `dict`-like 

93 Header to convert to standardized form. 

94 filename : `str`, optional 

95 Name of file being translated. 

96 

97 Returns 

98 ------- 

99 can : `bool` 

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

101 otherwise. 

102 """ 

103 # Check 3 headers that all have to match 

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

105 if k not in header: 

106 return False 

107 if header[k] != v: 

108 return False 

109 return True 

110 

111 @classmethod 

112 def compute_detector_num_from_name(cls, detector_group, detector_name): 

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

114 

115 Parameters 

116 ---------- 

117 detector_group : `str` 

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

119 detector_name : `str` 

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

121 

122 Returns 

123 ------- 

124 num : `int` 

125 Detector number. 

126 

127 Raises 

128 ------ 

129 ValueError 

130 The supplied name is not known. 

131 """ 

132 if detector_name != cls.DETECTOR_NAME: 

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

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

135 if group == detector_group: 

136 return num 

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

138 

139 @classmethod 

140 def compute_detector_group_from_num(cls, detector_num): 

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

142 

143 Parameters 

144 ---------- 

145 detector_num : `int` 

146 Detector number. 

147 

148 Returns 

149 ------- 

150 group : `str` 

151 Detector group. 

152 

153 Raises 

154 ------ 

155 ValueError 

156 The supplied number is not known. 

157 """ 

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

159 if num == detector_num: 

160 return group 

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

162 

163 @staticmethod 

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

165 """Helper method to calculate the exposure_id. 

166 

167 Parameters 

168 ---------- 

169 dateobs : `str` 

170 Date of observation in FITS ISO format. 

171 seqnum : `int`, unused 

172 Sequence number. Ignored. 

173 controller : `str`, unused 

174 Controller type. Ignored. 

175 

176 Returns 

177 ------- 

178 exposure_id : `int` 

179 Exposure ID. 

180 """ 

181 # Use 1 second resolution 

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

183 return int(exposure_id) 

184 

185 @cache_translation 

186 def to_datetime_begin(self): 

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

188 self._used_these_cards("MJD") 

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

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

191 

192 @cache_translation 

193 def to_detector_num(self): 

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

195 

196 Returns 

197 ------- 

198 num : `int` 

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

200 """ 

201 serial = self.to_detector_serial() 

202 return self._detector_map[serial][0] 

203 

204 @cache_translation 

205 def to_detector_group(self): 

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

207 

208 Returns 

209 ------- 

210 raft : `str` 

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

212 of the detector. 

213 """ 

214 serial = self.to_detector_serial() 

215 return self._detector_map[serial][1] 

216 

217 @cache_translation 

218 def to_physical_filter(self): 

219 """Return the filter name. 

220 

221 Uses the FILTER header. 

222 

223 Returns 

224 ------- 

225 filter : `str` 

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

227 """ 

228 

229 if "FILTER" in self._header: 

230 self._used_these_cards("FILTER") 

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

232 else: 

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

234 self.to_observation_id()) 

235 return "NONE" 

236 

237 def to_exposure_id(self): 

238 """Generate a unique exposure ID number 

239 

240 Note that SEQNUM is not unique for a given day 

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

242 integer. 

243 

244 Returns 

245 ------- 

246 exposure_id : `int` 

247 Unique exposure number. 

248 """ 

249 date = self.to_datetime_begin() 

250 return self.compute_exposure_id(date.isot) 

251 

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

253 to_visit_id = to_exposure_id 

254 

255 @cache_translation 

256 def to_observation_id(self): 

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

258 filename = self._header["FILENAME"] 

259 self._used_these_cards("FILENAME") 

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

261 

262 @cache_translation 

263 def to_science_program(self): 

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

265 

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

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

268 

269 Returns 

270 ------- 

271 run : `str` 

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

273 """ 

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

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

276 date.format = "iso" 

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

278 return str(date)