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

53 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-02-07 14:26 +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 

16import astropy.units as u 

17 

18from astro_metadata_translator import cache_translation 

19from astro_metadata_translator.translators.helpers import is_non_science 

20 

21from .lsst import LsstBaseTranslator, SIMONYI_TELESCOPE 

22 

23log = logging.getLogger(__name__) 

24 

25# Normalized name of the LSST Camera 

26LSST_CAM = "LSSTCam" 

27 

28 

29def is_non_science_or_lab(self): 

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

31 header. 

32 

33 Raises 

34 ------ 

35 KeyError 

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

37 """ 

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

39 # since the defaults are fine. 

40 try: 

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

42 is_non_science(self) 

43 return 

44 except KeyError: 

45 pass 

46 

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

48 if not self._is_on_mountain(): 

49 return 

50 

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

52 # use defaults. 

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

54 

55 

56class LsstCamTranslator(LsstBaseTranslator): 

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

58 

59 name = LSST_CAM 

60 """Name of this translation class""" 

61 

62 supported_instrument = LSST_CAM 

63 """Supports the lsstCam instrument.""" 

64 

65 _const_map = { 

66 "instrument": LSST_CAM, 

67 "telescope": SIMONYI_TELESCOPE, 

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

69 # includes them 

70 "altaz_begin": None, 

71 "object": "UNKNOWN", 

72 } 

73 

74 _trivial_map = { 

75 "detector_group": "RAFTBAY", 

76 "detector_name": "CCDSLOT", 

77 "observation_id": "OBSID", 

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

79 "detector_serial": "LSST_NUM", 

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

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

90 """Fix LSSTCam headers. 

91 

92 Notes 

93 ----- 

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

95 process. 

96 """ 

97 

98 modified = False 

99 

100 # Calculate the standard label to use for log messages 

101 log_label = cls._construct_log_prefix(obsid, filename) 

102 

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

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

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

106 

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

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

109 header["FILTER2"] = None 

110 modified = True 

111 

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

113 header["FILTER"] = "ph_5" 

114 modified = True 

115 

116 return modified 

117 

118 @classmethod 

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

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

121 supplied header. 

122 

123 Parameters 

124 ---------- 

125 header : `dict`-like 

126 Header to convert to standardized form. 

127 filename : `str`, optional 

128 Name of file being translated. 

129 

130 Returns 

131 ------- 

132 can : `bool` 

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

134 otherwise. 

135 """ 

136 # INSTRUME keyword might be of two types 

137 if "INSTRUME" in header: 

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

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

140 return True 

141 return False 

142 

143 @cache_translation 

144 def to_physical_filter(self): 

145 """Calculate the physical filter name. 

146 

147 Returns 

148 ------- 

149 filter : `str` 

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

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

152 are stripped. 

153 Returns "unknown" if no filter is declared. 

154 """ 

155 joined = super().to_physical_filter() 

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

157 joined = joined[:-len("~empty")] 

158 

159 return joined