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

88 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-20 13:11 +0000

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

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

189 def compute_detector_exposure_id(cls, exposure_id, detector_num): 

190 # Docstring inherited from LsstBaseTranslator. 

191 return compute_detector_exposure_id_generic(exposure_id, detector_num, max_num=cls.DETECTOR_MAX) 

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._log_prefix) 

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)