Coverage for python / astro_metadata_translator / translators / visit_info.py: 53%

70 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-14 23:38 +0000

1# This file is part of astro_metadata_translator. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the LICENSE file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# Use of this source code is governed by a 3-clause BSD-style 

10# license that can be found in the LICENSE file. 

11 

12"""Metadata translation code for afw VisitInfo headers.""" 

13 

14from __future__ import annotations 

15 

16__all__ = ("VisitInfoTranslator",) 

17 

18import logging 

19from collections.abc import Mapping, MutableMapping 

20from typing import TYPE_CHECKING, Any 

21 

22import astropy.time 

23import astropy.units as u 

24from astropy.coordinates import EarthLocation 

25 

26from ..translator import cache_translation 

27from .fits import FitsTranslator 

28from .helpers import altaz_from_degree_headers, tracking_from_degree_headers 

29 

30if TYPE_CHECKING: 

31 import astropy.coordinates 

32 

33log = logging.getLogger(__name__) 

34 

35 

36class VisitInfoTranslator(FitsTranslator): 

37 """Metadata translator for afw VisitInfo serialization. 

38 

39 VisitInfo is only defined for on-sky observations. 

40 

41 VisitInfo does not encode the following properties: 

42 

43 * observing_day 

44 * detector_serial 

45 * detector_unique_name 

46 * detector_group 

47 * detector_name 

48 

49 Some values can be found if there is butler provenance: 

50 

51 * detector_num 

52 * visit_id 

53 """ 

54 

55 name = "VisitInfo" 

56 """Name of this translation class""" 

57 

58 supported_instrument = None 

59 """Does not correspond to a single instrument.""" 

60 

61 default_resource_root = None 

62 """Corrections are not supported by this translator.""" 

63 

64 _const_map = { 

65 "detector_group": None, 

66 "detector_unique_name": None, 

67 "detector_serial": None, 

68 "detector_exposure_id": None, 

69 "detector_name": None, 

70 "physical_filter": None, 

71 } 

72 

73 _trivial_map: dict[str, str | list[str] | tuple[Any, ...]] = { 

74 "exposure_time": ("EXPTIME", {"unit": u.s}), 

75 "dark_time": ("DARKTIME", {"unit": u.s}), 

76 "boresight_airmass": "BORE-AIRMASS", 

77 "boresight_rotation_angle": ("BORE-ROTANG", {"unit": u.deg}), 

78 "observation_id": "IDNUM", 

79 "exposure_id": "IDNUM", 

80 "object": "OBJECT", 

81 "science_program": "PROGRAM", 

82 "telescope": ("TELESCOP", {"default": "Unknown"}), 

83 "instrument": "INSTRUMENT", 

84 "relative_humidity": "HUMIDITY", 

85 "temperature": ("AIRTEMP", {"unit": u.deg_C}), 

86 "pressure": ("AIRPRESS", {"unit": u.Pa}), 

87 "has_simulated_content": "HAS-SIMULATED-CONTENT", 

88 "observation_type": "OBSTYPE", 

89 "focus_z": ("FOCUSZ", {"unit": u.mm}), 

90 "observation_reason": "REASON", 

91 # Rely on butler provenance. 

92 "detector_num": "LSST BUTLER DATAID DETECTOR", 

93 } 

94 

95 @classmethod 

96 def can_translate(cls, header: Mapping[str, Any], filename: str | None = None) -> bool: 

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

98 supplied header. 

99 

100 Always returns `False`. This translator has to be selected explicitly 

101 from context. 

102 

103 Parameters 

104 ---------- 

105 header : `dict`-like 

106 Header to convert to standardized form. 

107 filename : `str`, optional 

108 Name of file being translated. 

109 

110 Returns 

111 ------- 

112 can : `bool` 

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

114 otherwise. 

115 """ 

116 return False 

117 

118 @cache_translation 

119 def to_observation_counter(self) -> int: 

120 """Return the exposure ID as proxy for counter. 

121 

122 Some exposure IDs encode a year and counter in the integer, others 

123 simply have an incrementing counter 

124 

125 Returns 

126 ------- 

127 sequence : `int` 

128 The observation counter. 

129 """ 

130 return self.to_exposure_id() 

131 

132 @cache_translation 

133 def to_boresight_rotation_coord(self) -> str: 

134 # Docstring will be inherited. 

135 if self.is_key_ok("ROTTYPE"): 

136 self._used_these_cards("ROTTYPE") 

137 return self._header["ROTTYPE"].lower() 

138 return "unknown" 

139 

140 @cache_translation 

141 def to_visit_id(self) -> int: 

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

143 # This can be found in butler provenance in some cases. 

144 # It is not generally used though and visit_id is effectively 

145 # deprecated. 

146 prov_key = "LSST BUTLER DATAID VISIT" 

147 if self.is_key_ok(prov_key): 

148 self._used_these_cards(prov_key) 

149 return self._header[prov_key] 

150 return self.to_exposure_id() 

151 

152 @cache_translation 

153 def to_datetime_begin(self) -> astropy.time.Time | None: 

154 if self.is_key_ok("DATE-AVG"): 

155 date_avg = self._from_fits_date("DATE-AVG", scale="tai") 

156 self._used_these_cards("DATE-AVG") 

157 return date_avg - (self.to_exposure_time() / 2.0) 

158 return None 

159 

160 @cache_translation 

161 def to_datetime_end(self) -> astropy.time.Time: 

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

163 datetime_end = self.to_datetime_begin() 

164 if datetime_end is not None: 

165 datetime_end = self.to_datetime_begin() + self.to_exposure_time() 

166 return datetime_end 

167 

168 @cache_translation 

169 def to_location(self) -> astropy.coordinates.EarthLocation: 

170 """Calculate the observatory location. 

171 

172 Returns 

173 ------- 

174 location : `astropy.coordinates.EarthLocation` 

175 An object representing the location of the telescope. 

176 """ 

177 # OBS-LONG is east positive for VisitInfo. 

178 lon = self._header["OBS-LONG"] 

179 value = EarthLocation.from_geodetic(lon, self._header["OBS-LAT"], self._header["OBS-ELEV"]) 

180 self._used_these_cards("OBS-LONG", "OBS-LAT", "OBS-ELEV") 

181 return value 

182 

183 @cache_translation 

184 def to_tracking_radec(self) -> astropy.coordinates.SkyCoord | None: 

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

186 radecpairs = (("BORE-RA", "BORE-DEC"),) 

187 return tracking_from_degree_headers(self, "ICRS", radecpairs, unit=(u.deg, u.deg)) 

188 

189 @cache_translation 

190 def to_altaz_begin(self) -> astropy.coordinates.AltAz | None: 

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

192 return altaz_from_degree_headers(self, (("BORE-ALT", "BORE-AZ"),), self.to_datetime_begin()) 

193 

194 @classmethod 

195 def fix_header( 

196 cls, header: MutableMapping[str, Any], instrument: str, obsid: str, filename: str | None = None 

197 ) -> bool: 

198 """Fix DECam headers. 

199 

200 Parameters 

201 ---------- 

202 header : `dict` 

203 The header to update. Updates are in place. 

204 instrument : `str` 

205 The name of the instrument. 

206 obsid : `str` 

207 Unique observation identifier associated with this header. 

208 Will always be provided. 

209 filename : `str`, optional 

210 Filename associated with this header. May not be set since headers 

211 can be fixed independently of any filename being known. 

212 

213 Returns 

214 ------- 

215 modified : `bool` 

216 Returns `True` if the header was updated. 

217 

218 Notes 

219 ----- 

220 No fixes are applied for VisitInfo at this time. 

221 """ 

222 modified = False 

223 return modified