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 # 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 @classmethod 

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

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

85 supplied header. 

86 

87 Parameters 

88 ---------- 

89 header : `dict`-like 

90 Header to convert to standardized form. 

91 filename : `str`, optional 

92 Name of file being translated. 

93 

94 Returns 

95 ------- 

96 can : `bool` 

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

98 otherwise. 

99 """ 

100 # Check 3 headers that all have to match 

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

102 if k not in header: 

103 return False 

104 if header[k] != v: 

105 return False 

106 return True 

107 

108 @classmethod 

109 def compute_detector_num_from_name(cls, detector_group, detector_name): 

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

111 

112 Parameters 

113 ---------- 

114 detector_group : `str` 

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

116 detector_name : `str` 

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

118 

119 Returns 

120 ------- 

121 num : `int` 

122 Detector number. 

123 

124 Raises 

125 ------ 

126 ValueError 

127 The supplied name is not known. 

128 """ 

129 if detector_name != cls.DETECTOR_NAME: 

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

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

132 if group == detector_group: 

133 return num 

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

135 

136 @classmethod 

137 def compute_detector_group_from_num(cls, detector_num): 

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

139 

140 Parameters 

141 ---------- 

142 detector_num : `int` 

143 Detector number. 

144 

145 Returns 

146 ------- 

147 group : `str` 

148 Detector group. 

149 

150 Raises 

151 ------ 

152 ValueError 

153 The supplied number is not known. 

154 """ 

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

156 if num == detector_num: 

157 return group 

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

159 

160 @staticmethod 

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

162 """Helper method to calculate the exposure_id. 

163 

164 Parameters 

165 ---------- 

166 dateobs : `str` 

167 Date of observation in FITS ISO format. 

168 seqnum : `int`, unused 

169 Sequence number. Ignored. 

170 controller : `str`, unused 

171 Controller type. Ignored. 

172 

173 Returns 

174 ------- 

175 exposure_id : `int` 

176 Exposure ID. 

177 """ 

178 # Use 1 second resolution 

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

180 return int(exposure_id) 

181 

182 @cache_translation 

183 def to_datetime_begin(self): 

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

185 self._used_these_cards("MJD") 

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

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

188 

189 @cache_translation 

190 def to_detector_num(self): 

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

192 

193 Returns 

194 ------- 

195 num : `int` 

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

197 """ 

198 serial = self.to_detector_serial() 

199 return self._detector_map[serial][0] 

200 

201 @cache_translation 

202 def to_detector_group(self): 

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

204 

205 Returns 

206 ------- 

207 raft : `str` 

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

209 of the detector. 

210 """ 

211 serial = self.to_detector_serial() 

212 return self._detector_map[serial][1] 

213 

214 @cache_translation 

215 def to_physical_filter(self): 

216 """Return the filter name. 

217 

218 Uses the FILTER header. 

219 

220 Returns 

221 ------- 

222 filter : `str` 

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

224 """ 

225 

226 if "FILTER" in self._header: 

227 self._used_these_cards("FILTER") 

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

229 else: 

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

231 self.to_observation_id()) 

232 return "NONE" 

233 

234 def to_exposure_id(self): 

235 """Generate a unique exposure ID number 

236 

237 Note that SEQNUM is not unique for a given day 

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

239 integer. 

240 

241 Returns 

242 ------- 

243 exposure_id : `int` 

244 Unique exposure number. 

245 """ 

246 date = self.to_datetime_begin() 

247 return self.compute_exposure_id(date.isot) 

248 

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

250 to_visit_id = to_exposure_id 

251 

252 @cache_translation 

253 def to_observation_id(self): 

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

255 filename = self._header["FILENAME"] 

256 self._used_these_cards("FILENAME") 

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

258 

259 @cache_translation 

260 def to_science_program(self): 

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

262 

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

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

265 

266 Returns 

267 ------- 

268 run : `str` 

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

270 """ 

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

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

273 date.format = "iso" 

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

275 return str(date)