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

64 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-03-16 10:13 +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 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_LSST_CAM_SHIP_DATE = 202406 

32 

33 

34def is_non_science_or_lab(self): 

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

36 header. 

37 

38 Raises 

39 ------ 

40 KeyError 

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

42 """ 

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

44 # since the defaults are fine. 

45 try: 

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

47 is_non_science(self) 

48 return 

49 except KeyError: 

50 pass 

51 

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

53 if not self._is_on_mountain(): 

54 return 

55 

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

57 # use defaults. 

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

59 

60 

61class LsstCamTranslator(LsstBaseTranslator): 

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

63 

64 name = LSST_CAM 

65 """Name of this translation class""" 

66 

67 supported_instrument = LSST_CAM 

68 """Supports the lsstCam instrument.""" 

69 

70 _const_map = { 

71 "instrument": LSST_CAM, 

72 "telescope": SIMONYI_TELESCOPE, 

73 # Migrate these to full translations once test data appears that 

74 # includes them 

75 "altaz_begin": None, 

76 "object": "UNKNOWN", 

77 } 

78 

79 _trivial_map = { 

80 "detector_group": "RAFTBAY", 

81 "detector_name": "CCDSLOT", 

82 "observation_id": "OBSID", 

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

84 "detector_serial": "LSST_NUM", 

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

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

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

88 } 

89 

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

91 cameraPolicyFile = "policy/lsstCam.yaml" 

92 

93 @classmethod 

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

95 """Fix LSSTCam headers. 

96 

97 Notes 

98 ----- 

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

100 process. 

101 """ 

102 

103 modified = False 

104 

105 # Calculate the standard label to use for log messages 

106 log_label = cls._construct_log_prefix(obsid, filename) 

107 

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

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

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

111 

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

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

114 header["FILTER2"] = None 

115 modified = True 

116 

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

118 header["FILTER"] = "ph_5" 

119 modified = True 

120 

121 return modified 

122 

123 @classmethod 

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

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

126 supplied header. 

127 

128 Parameters 

129 ---------- 

130 header : `dict`-like 

131 Header to convert to standardized form. 

132 filename : `str`, optional 

133 Name of file being translated. 

134 

135 Returns 

136 ------- 

137 can : `bool` 

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

139 otherwise. 

140 """ 

141 # INSTRUME keyword might be of two types 

142 if "INSTRUME" in header: 

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

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

145 return True 

146 return False 

147 

148 @cache_translation 

149 def to_physical_filter(self): 

150 """Calculate the physical filter name. 

151 

152 Returns 

153 ------- 

154 filter : `str` 

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

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

157 are stripped. 

158 Returns "unknown" if no filter is declared. 

159 """ 

160 joined = super().to_physical_filter() 

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

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

163 

164 return joined 

165 

166 @classmethod 

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

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

169 

170 Parameters 

171 ---------- 

172 observing_date : `astropy.time.Time` 

173 The date of the observation. Unused. 

174 

175 Returns 

176 ------- 

177 offset : `astropy.time.TimeDelta` 

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

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

180 In Chile the offset is always UTC-12. 

181 """ 

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

183 # is in the lab. 

184 if int(observing_date.strftime("%Y%m")) > _LSST_CAM_SHIP_DATE: 

185 return cls._ROLLOVER_TIME # 12 hours in base class 

186 

187 # Convert the date to a datetime UTC. 

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

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

190 

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

192 offset = pacific_time.utcoffset() * -1 

193 return astropy.time.TimeDelta(offset)