Coverage for python / lsst / obs / lsst / translators / ts3.py: 66%

58 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-30 09:16 +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 BNL TestStand 3 headers""" 

12 

13__all__ = ("LsstTS3Translator", ) 

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 a single sensor at a time so define a 

29# fixed sensor name 

30_DETECTOR_NAME = "S00" 

31 

32 

33class LsstTS3Translator(LsstBaseTranslator): 

34 """Metadata translator for LSST BNL Test Stand 3 data. 

35 """ 

36 

37 name = "LSST-TS3" 

38 """Name of this translation class""" 

39 

40 _const_map = { 

41 # TS3 is not attached to a telescope so many translations are null. 

42 "instrument": "LSST-TS3", 

43 "telescope": None, 

44 "location": None, 

45 "boresight_rotation_coord": None, 

46 "boresight_rotation_angle": None, 

47 "boresight_airmass": None, 

48 "tracking_radec": None, 

49 "altaz_begin": None, 

50 "object": "UNKNOWN", 

51 "relative_humidity": None, 

52 "temperature": None, 

53 "pressure": None, 

54 "detector_name": _DETECTOR_NAME, # Single sensor 

55 "can_see_sky": False, 

56 } 

57 

58 _trivial_map = { 

59 "detector_serial": "LSST_NUM", 

60 "physical_filter": "FILTER", 

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

62 } 

63 

64 DETECTOR_NAME = _DETECTOR_NAME 

65 """Fixed name of single sensor.""" 

66 

67 cameraPolicyFile = "policy/ts3.yaml" 

68 

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

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

71 

72 @classmethod 

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

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

75 supplied header. 

76 

77 There is no usable ``INSTRUME`` header in TS3 data. Instead we use 

78 the ``TSTAND`` header. 

79 

80 Parameters 

81 ---------- 

82 header : `dict`-like 

83 Header to convert to standardized form. 

84 filename : `str`, optional 

85 Name of file being translated. 

86 

87 Returns 

88 ------- 

89 can : `bool` 

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

91 otherwise. 

92 """ 

93 return cls.can_translate_with_options(header, {"TSTAND": "BNL-TS3-2-Janeway"}, filename=filename) 

94 

95 @staticmethod 

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

97 """Helper method to calculate the TS3 exposure_id. 

98 

99 Parameters 

100 ---------- 

101 dateobs : `str` 

102 Date of observation in FITS ISO format. 

103 seqnum : `int`, unused 

104 Sequence number. Ignored. 

105 controller : `str`, unused 

106 Controller type. Ignored. 

107 

108 Returns 

109 ------- 

110 exposure_id : `int` 

111 Exposure ID. 

112 """ 

113 # There is worry that seconds are too coarse so use 10th of second 

114 # and read the first 21 characters. 

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

116 return int(exposure_id) 

117 

118 @classmethod 

119 def compute_detector_exposure_id(cls, exposure_id, detector_num): 

120 # Docstring inherited from LsstBaseTranslator. 

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

122 

123 @cache_translation 

124 def to_datetime_begin(self): 

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

126 self._used_these_cards("MJD-OBS") 

127 return Time(self._header["MJD-OBS"], scale="utc", format="mjd") 

128 

129 def to_exposure_id(self): 

130 """Generate a unique exposure ID number 

131 

132 Note that SEQNUM is not unique for a given day in TS3 data 

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

134 integer. 

135 

136 Returns 

137 ------- 

138 exposure_id : `int` 

139 Unique exposure number. 

140 """ 

141 iso = self._header["DATE-OBS"] 

142 self._used_these_cards("DATE-OBS") 

143 

144 return self.compute_exposure_id(iso) 

145 

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

147 to_visit_id = to_exposure_id 

148 

149 @cache_translation 

150 def to_science_program(self): 

151 """Calculate the science program information. 

152 

153 There is no header recording this in TS3 data so instead return 

154 the observing day in YYYY-MM-DD format. 

155 

156 Returns 

157 ------- 

158 run : `str` 

159 Observing day in YYYY-MM-DD format. 

160 """ 

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

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

163 date.format = "iso" 

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

165 return str(date) 

166 

167 @cache_translation 

168 def to_observation_id(self): 

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

170 filename = self._header["FILENAME"] 

171 self._used_these_cards("FILENAME") 

172 return os.path.splitext(filename)[0] 

173 

174 @cache_translation 

175 def to_detector_group(self): 

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

177 serial = self.to_detector_serial() 

178 detector_info = self.compute_detector_info_from_serial(serial) 

179 return detector_info[0] 

180 

181 @classmethod 

182 def max_exposure_id(cls): 

183 # Only one controller by definition and only the date matters. 

184 return cls.compute_exposure_id("2050-12-31T23:59.999")