Coverage for python/lsst/obs/lsst/translators/lsstCam.py: 43%

64 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-05-02 04:44 -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 the main LSST Camera""" 

12 

13__all__ = ("LsstCamTranslator", ) 

14 

15import logging 

16 

17import pytz 

18import astropy.time 

19import astropy.units as u 

20 

21from astro_metadata_translator import cache_translation 

22from astro_metadata_translator.translators.helpers import is_non_science 

23 

24from .lsst import LsstBaseTranslator, SIMONYI_TELESCOPE 

25 

26log = logging.getLogger(__name__) 

27 

28# Normalized name of the LSST Camera 

29LSST_CAM = "LSSTCam" 

30 

31 

32def is_non_science_or_lab(self): 

33 """Pseudo method to determine whether this is a lab or non-science 

34 header. 

35 

36 Raises 

37 ------ 

38 KeyError 

39 If this is a science observation and on the mountain. 

40 """ 

41 # Return without raising if this is not a science observation 

42 # since the defaults are fine. 

43 try: 

44 # This will raise if it is a science observation. 

45 is_non_science(self) 

46 return 

47 except KeyError: 

48 pass 

49 

50 # We are still in the lab, return and use the default. 

51 if not self._is_on_mountain(): 

52 return 

53 

54 # This is a science observation on the mountain so we should not 

55 # use defaults. 

56 raise KeyError(f"{self._log_prefix}: Required key is missing and this is a mountain science observation") 

57 

58 

59class LsstCamTranslator(LsstBaseTranslator): 

60 """Metadata translation for the main LSST Camera.""" 

61 

62 name = LSST_CAM 

63 """Name of this translation class""" 

64 

65 supported_instrument = LSST_CAM 

66 """Supports the lsstCam instrument.""" 

67 

68 _const_map = { 

69 "instrument": LSST_CAM, 

70 "telescope": SIMONYI_TELESCOPE, 

71 } 

72 

73 _trivial_map = { 

74 "detector_group": "RAFTBAY", 

75 "detector_name": "CCDSLOT", 

76 "observation_id": "OBSID", 

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

78 "detector_serial": "LSST_NUM", 

79 "object": ("OBJECT", dict(default="UNKNOWN")), 

80 "science_program": (["PROGRAM", "RUNNUM"], dict(default="unknown")), 

81 "boresight_rotation_angle": (["ROTPA", "ROTANGLE"], dict(checker=is_non_science_or_lab, 

82 default=0.0, unit=u.deg)), 

83 } 

84 

85 # Use Imsim raft definitions until a true lsstCam definition exists 

86 cameraPolicyFile = "policy/lsstCam.yaml" 

87 

88 # Date (YYYYMM) the camera changes from using lab day_offset (Pacific time) 

89 # to summit day_offset (12 hours). 

90 _CAMERA_SHIP_DATE = 202406 

91 

92 @classmethod 

93 def fix_header(cls, header, instrument, obsid, filename=None): 

94 """Fix LSSTCam headers. 

95 

96 Notes 

97 ----- 

98 See `~astro_metadata_translator.fix_header` for details of the general 

99 process. 

100 """ 

101 

102 modified = False 

103 

104 # Calculate the standard label to use for log messages 

105 log_label = cls._construct_log_prefix(obsid, filename) 

106 

107 if "FILTER" not in header and header.get("FILTER2") is not None: 

108 ccdslot = header.get("CCDSLOT", "unknown") 

109 raftbay = header.get("RAFTBAY", "unknown") 

110 

111 log.warning("%s %s_%s: No FILTER key found but FILTER2=\"%s\" (removed)", 

112 log_label, raftbay, ccdslot, header["FILTER2"]) 

113 header["FILTER2"] = None 

114 modified = True 

115 

116 if header.get("DAYOBS") in ("20231107", "20231108") and header["FILTER"] == "ph_05": 

117 header["FILTER"] = "ph_5" 

118 modified = True 

119 

120 return modified 

121 

122 @classmethod 

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

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

125 supplied header. 

126 

127 Parameters 

128 ---------- 

129 header : `dict`-like 

130 Header to convert to standardized form. 

131 filename : `str`, optional 

132 Name of file being translated. 

133 

134 Returns 

135 ------- 

136 can : `bool` 

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

138 otherwise. 

139 """ 

140 # INSTRUME keyword might be of two types 

141 if "INSTRUME" in header: 

142 instrume = header["INSTRUME"].lower() 

143 if instrume == cls.supported_instrument.lower(): 

144 return True 

145 return False 

146 

147 @cache_translation 

148 def to_physical_filter(self): 

149 """Calculate the physical filter name. 

150 

151 Returns 

152 ------- 

153 filter : `str` 

154 Name of filter. Can be a combination of FILTER, FILTER1, and 

155 FILTER2 headers joined by a "~". Trailing "~empty" components 

156 are stripped. 

157 Returns "unknown" if no filter is declared. 

158 """ 

159 joined = super().to_physical_filter() 

160 while joined.endswith("~empty"): 

161 joined = joined.removesuffix("~empty") 

162 

163 return joined 

164 

165 @classmethod 

166 def observing_date_to_offset(cls, observing_date: astropy.time.Time) -> astropy.time.TimeDelta | None: 

167 """Return the offset to use when calculating the observing day. 

168 

169 Parameters 

170 ---------- 

171 observing_date : `astropy.time.Time` 

172 The date of the observation. Unused. 

173 

174 Returns 

175 ------- 

176 offset : `astropy.time.TimeDelta` 

177 The offset to apply. During lab testing the offset is Pacific 

178 Time which can mean UTC-7 or UTC-8 depending on daylight savings. 

179 In Chile the offset is always UTC-12. 

180 """ 

181 # Timezone calculations are slow. Only do this if the instrument 

182 # is in the lab. 

183 if int(observing_date.strftime("%Y%m")) > cls._CAMERA_SHIP_DATE: 

184 return cls._ROLLOVER_TIME # 12 hours in base class 

185 

186 # Convert the date to a datetime UTC. 

187 pacific_tz = pytz.timezone("US/Pacific") 

188 pacific_time = observing_date.utc.to_datetime(timezone=pacific_tz) 

189 

190 # We need the offset to go the other way. 

191 offset = pacific_time.utcoffset() * -1 

192 return astropy.time.TimeDelta(offset)