Coverage for python/lsst/obs/lsst/translators/lsst_ucdcam.py: 45%

85 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-20 03:16 -0700

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 = 10 

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

80 detector_exposure_id. 

81 

82 This is rounded up to a power of ten to make those IDs human-decodable. 

83 """ 

84 

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

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

87 

88 @classmethod 

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

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

91 supplied header. 

92 

93 Parameters 

94 ---------- 

95 header : `dict`-like 

96 Header to convert to standardized form. 

97 filename : `str`, optional 

98 Name of file being translated. 

99 

100 Returns 

101 ------- 

102 can : `bool` 

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

104 otherwise. 

105 """ 

106 # Check 3 headers that all have to match 

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

108 if k not in header: 

109 return False 

110 if header[k] != v: 

111 return False 

112 return True 

113 

114 @classmethod 

115 def compute_detector_num_from_name(cls, detector_group, detector_name): 

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

117 

118 Parameters 

119 ---------- 

120 detector_group : `str` 

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

122 detector_name : `str` 

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

124 

125 Returns 

126 ------- 

127 num : `int` 

128 Detector number. 

129 

130 Raises 

131 ------ 

132 ValueError 

133 The supplied name is not known. 

134 """ 

135 if detector_name != cls.DETECTOR_NAME: 

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

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

138 if group == detector_group: 

139 return num 

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

141 

142 @classmethod 

143 def compute_detector_group_from_num(cls, detector_num): 

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

145 

146 Parameters 

147 ---------- 

148 detector_num : `int` 

149 Detector number. 

150 

151 Returns 

152 ------- 

153 group : `str` 

154 Detector group. 

155 

156 Raises 

157 ------ 

158 ValueError 

159 The supplied number is not known. 

160 """ 

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

162 if num == detector_num: 

163 return group 

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

165 

166 @staticmethod 

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

168 """Helper method to calculate the exposure_id. 

169 

170 Parameters 

171 ---------- 

172 dateobs : `str` 

173 Date of observation in FITS ISO format. 

174 seqnum : `int`, unused 

175 Sequence number. Ignored. 

176 controller : `str`, unused 

177 Controller type. Ignored. 

178 

179 Returns 

180 ------- 

181 exposure_id : `int` 

182 Exposure ID. 

183 """ 

184 # Use 1 second resolution 

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

186 return int(exposure_id) 

187 

188 @cache_translation 

189 def to_datetime_begin(self): 

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

191 self._used_these_cards("MJD") 

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

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

194 

195 @cache_translation 

196 def to_detector_num(self): 

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

198 

199 Returns 

200 ------- 

201 num : `int` 

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

203 """ 

204 serial = self.to_detector_serial() 

205 return self._detector_map[serial][0] 

206 

207 @cache_translation 

208 def to_detector_group(self): 

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

210 

211 Returns 

212 ------- 

213 raft : `str` 

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

215 of the detector. 

216 """ 

217 serial = self.to_detector_serial() 

218 return self._detector_map[serial][1] 

219 

220 @cache_translation 

221 def to_physical_filter(self): 

222 """Return the filter name. 

223 

224 Uses the FILTER header. 

225 

226 Returns 

227 ------- 

228 filter : `str` 

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

230 """ 

231 

232 if "FILTER" in self._header: 

233 self._used_these_cards("FILTER") 

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

235 else: 

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

237 self._log_prefix) 

238 return "NONE" 

239 

240 def to_exposure_id(self): 

241 """Generate a unique exposure ID number 

242 

243 Note that SEQNUM is not unique for a given day 

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

245 integer. 

246 

247 Returns 

248 ------- 

249 exposure_id : `int` 

250 Unique exposure number. 

251 """ 

252 date = self.to_datetime_begin() 

253 return self.compute_exposure_id(date.isot) 

254 

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

256 to_visit_id = to_exposure_id 

257 

258 @cache_translation 

259 def to_observation_id(self): 

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

261 filename = self._header["FILENAME"] 

262 self._used_these_cards("FILENAME") 

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

264 

265 @cache_translation 

266 def to_science_program(self): 

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

268 

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

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

271 

272 Returns 

273 ------- 

274 run : `str` 

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

276 """ 

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

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

279 date.format = "iso" 

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

281 return str(date)