Coverage for python / lsst / obs / lsst / translators / comCamSim.py: 27%

75 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-22 09:22 +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 Simulated LSST Commissioning Camera""" 

12 

13__all__ = ("LsstComCamSimTranslator", ) 

14 

15import logging 

16import math 

17 

18import astropy 

19import astropy.units as u 

20import astropy.utils.exceptions 

21from astropy.coordinates import AltAz 

22from astro_metadata_translator import cache_translation 

23 

24from .lsstCam import LsstCamTranslator 

25from .lsst import SIMONYI_TELESCOPE 

26 

27log = logging.getLogger(__name__) 

28 

29 

30class LsstComCamSimTranslator(LsstCamTranslator): 

31 """Metadata translation for the LSST Commissioning Camera.""" 

32 

33 name = "LSSTComCamSim" 

34 """Name of this translation class""" 

35 

36 _const_map = { 

37 "instrument": "LSSTComCamSim", 

38 "has_simulated_content": True, 

39 } 

40 

41 cameraPolicyFile = 'policy/comCamSim.yaml' 

42 

43 # Allowed to use obstype for can_see_sky fallback. 

44 _can_check_obstype_for_can_see_sky = True 

45 

46 @classmethod 

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

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

49 supplied header. 

50 

51 Looks for "COMCAMSIM" instrument in case-insensitive manner but 

52 must be on LSST telescope. This avoids confusion with other 

53 telescopes using commissioning cameras. 

54 

55 Parameters 

56 ---------- 

57 header : `dict`-like 

58 Header to convert to standardized form. 

59 filename : `str`, optional 

60 Name of file being translated. 

61 

62 Returns 

63 ------- 

64 can : `bool` 

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

66 otherwise. 

67 """ 

68 if "INSTRUME" in header and "TELESCOP" in header: 

69 telescope = header["TELESCOP"] 

70 instrument = header["INSTRUME"].lower() 

71 if instrument == "comcamsim" and telescope in (SIMONYI_TELESCOPE, "LSST"): 

72 return True 

73 

74 return False 

75 

76 @classmethod 

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

78 """Fix Simulated ComCam headers. 

79 

80 Notes 

81 ----- 

82 Content will be added as needed. 

83 

84 Corrections are reported as debug level log messages. 

85 

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

87 process. 

88 """ 

89 modified = False 

90 

91 # Calculate the standard label to use for log messages 

92 log_label = cls._construct_log_prefix(obsid, filename) 

93 

94 # Some simulated files lack RASTART/DECSTART etc headers. Since these 

95 # are simulated they can be populated directly from the RA/DEC headers. 

96 synced_radec = False 

97 for key in ("RA", "DEC"): 

98 for time in ("START", "END"): 

99 time_key = f"{key}{time}" 

100 if not header.get(time_key): 

101 if (value := header.get(key)): 

102 header[time_key] = value 

103 synced_radec = True 

104 if synced_radec: 

105 modified = True 

106 log.debug("%s: Synced RASTART/RAEND/DECSTART/DECEND headers with RA/DEC headers", log_label) 

107 

108 if not header.get("RADESYS") and header.get("RA") and header.get("DEC"): 

109 header["RADESYS"] = "ICRS" 

110 log.debug("%s: Forcing undefined RADESYS to '%s'", log_label, header["RADESYS"]) 

111 modified = True 

112 

113 if not header.get("TELCODE"): 

114 if camcode := header.get("CAMCODE"): 

115 header["TELCODE"] = camcode 

116 modified = True 

117 log.debug("%s: Setting TELCODE header from CAMCODE header", log_label) 

118 else: 

119 # Get the code from the OBSID. 

120 code, _ = obsid.split("_", 1) 

121 header["TELCODE"] = code 

122 modified = True 

123 log.debug("%s: Determining telescope code of %s from OBSID", log_label, code) 

124 

125 return modified 

126 

127 def _is_on_mountain(self): 

128 """Indicate whether these data are coming from the instrument 

129 installed on the mountain. 

130 Returns 

131 ------- 

132 is : `bool` 

133 `True` if instrument is on the mountain. 

134 

135 Notes 

136 ----- 

137 TODO: DM-33387 This is currently a terrible hack and MUST be removed 

138 once CAP-807 and CAP-808 are done. 

139 Until then, ALL non-calib ComCam data will look like it is on sky. 

140 """ 

141 return True 

142 

143 @cache_translation 

144 def to_altaz_begin(self): 

145 # Tries to calculate the value. Simulated files for ops-rehearsal 3 

146 # did not have the AZ/EL headers defined. 

147 if self.are_keys_ok(["ELSTART", "AZSTART"]): 

148 return super().to_altaz_begin() 

149 

150 # The time is not consistent with HASTART/AMSTART values. 

151 # This means that the elevation may well come out negative. 

152 # Rather than attempting to calculate something that is already 

153 # known to be junk, return a fixed value. 

154 if self.are_keys_ok(["RA", "DEC"]): 

155 

156 # If there is an airmass value, use it for the elevation 

157 # to try to use the available information even if inconsistent 

158 # with the observing date. 

159 airmass = self.to_boresight_airmass() 

160 if airmass is None: 

161 elevation = 45 * u.deg 

162 else: 

163 # The number does not have to be accurate. 

164 elevation = math.asin(1 / airmass) * u.rad 

165 

166 return AltAz( 

167 0. * u.deg, elevation, obstime=self.to_datetime_begin(), location=self.to_location() 

168 ) 

169 

170 return None 

171 

172 @cache_translation 

173 def to_altaz_end(self): 

174 # Tries to calculate the value. Simulated files for ops-rehearsal 3 

175 # did not have the AZ/EL headers defined. 

176 if self.are_keys_ok(["ELEND", "AZEND"]): 

177 return super().to_altaz_end() 

178 # Do not attempt to calculate anything in this situation since it is 

179 # never going to be correct. 

180 return None 

181 

182 @classmethod 

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

184 # Always use the 12 hour offset. 

185 return cls._ROLLOVER_TIME