Coverage for python / lsst / obs / base / makeRawVisitInfoViaObsInfo.py: 15%

103 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-04 01:44 -0800

1# This file is part of obs_base. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

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

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

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <http://www.gnu.org/licenses/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ["MakeRawVisitInfoViaObsInfo"] 

25 

26import logging 

27import warnings 

28from typing import TYPE_CHECKING, Any, ClassVar 

29 

30import astropy.units 

31import astropy.utils.exceptions 

32from astropy.utils import iers 

33 

34# Prefer the standard pyerfa over the Astropy version 

35try: 

36 import erfa 

37 

38 ErfaWarning = erfa.ErfaWarning 

39except ImportError: 

40 import astropy._erfa as erfa 

41 

42 ErfaWarning = None 

43 

44from astro_metadata_translator import MetadataTranslator, ObservationInfo 

45 

46from lsst.afw.coord import Observatory, Weather 

47from lsst.afw.image import RotType, VisitInfo 

48from lsst.daf.base import DateTime 

49from lsst.geom import SpherePoint, degrees, radians 

50 

51if TYPE_CHECKING: 

52 import lsst.afw.image 

53 import lsst.daf.base 

54 

55 

56class MakeRawVisitInfoViaObsInfo: 

57 """Base class functor to make a VisitInfo from the FITS header of a 

58 raw image using `~astro_metadata_translator.ObservationInfo` translators. 

59 

60 Subclasses can be used if a specific 

61 `~astro_metadata_translator.MetadataTranslator` translator should be used. 

62 

63 The design philosophy is to make a best effort and log warnings of 

64 problems, rather than raising exceptions, in order to extract as much 

65 VisitInfo information as possible from a messy FITS header without the 

66 user needing to add a lot of error handling. 

67 

68 Parameters 

69 ---------- 

70 log : `logging.Logger` or None 

71 Logger to use for messages. If `None` uses a logger named 

72 "lsst.obs.base.makeRawVisitInfoViaObsInfo". 

73 doStripHeader : `bool`, optional 

74 Strip header keywords from the metadata as they are used? 

75 """ 

76 

77 metadataTranslator: ClassVar[type[MetadataTranslator] | None] = None 

78 """Header translator to use to construct VisitInfo, defaulting to 

79 automatic determination.""" 

80 

81 def __init__(self, log: logging.Logger | None = None, doStripHeader: bool = False): 

82 if log is None: 

83 log = logging.getLogger(__name__) 

84 self.log = log 

85 self.doStripHeader = doStripHeader 

86 

87 def __call__(self, md: lsst.daf.base.PropertySet) -> lsst.afw.image.VisitInfo: 

88 """Construct a VisitInfo and strip associated data from the metadata. 

89 

90 Parameters 

91 ---------- 

92 md : `lsst.daf.base.PropertyList` or `lsst.daf.base.PropertySet` 

93 Metadata to pull from. 

94 May be modified if ``stripHeader`` is ``True``. 

95 

96 Returns 

97 ------- 

98 visitInfo : `lsst.afw.image.VisitInfo` 

99 `~lsst.afw.image.VisitInfo` derived from the header using 

100 a `~astro_metadata_translator.MetadataTranslator`. 

101 """ 

102 obsInfo = ObservationInfo(md, translator_class=self.metadataTranslator) 

103 

104 if self.doStripHeader: 

105 # Strip all the cards out that were used 

106 for c in obsInfo.cards_used: 

107 del md[c] 

108 

109 return self.observationInfo2visitInfo(obsInfo, log=self.log) 

110 

111 @staticmethod 

112 def observationInfo2visitInfo( 

113 obsInfo: ObservationInfo, log: logging.Logger | None = None 

114 ) -> lsst.afw.image.VisitInfo: 

115 """Construct a `~lsst.afw.image.VisitInfo` from an 

116 `~astro_metadata_translator.ObservationInfo`. 

117 

118 Parameters 

119 ---------- 

120 obsInfo : `astro_metadata_translator.ObservationInfo` 

121 Information gathered from the observation metadata. 

122 log : `logging.Logger` or `lsst.log.Log`, optional 

123 Logger to use for logging informational messages. 

124 If `None` logging will be disabled. 

125 

126 Returns 

127 ------- 

128 visitInfo : `lsst.afw.image.VisitInfo` 

129 `~lsst.afw.image.VisitInfo` derived from the supplied 

130 `~astro_metadata_translator.ObservationInfo`. 

131 """ 

132 argDict: dict[str, Any] = {} 

133 

134 # Map the translated information into a form suitable for VisitInfo 

135 if obsInfo.exposure_time is not None: 

136 argDict["exposureTime"] = float(obsInfo.exposure_time.to_value("s")) 

137 if obsInfo.dark_time is not None: 

138 argDict["darkTime"] = float(obsInfo.dark_time.to_value("s")) 

139 argDict["id"] = obsInfo.exposure_id 

140 argDict["instrumentLabel"] = obsInfo.instrument 

141 if obsInfo.focus_z is not None: 

142 argDict["focusZ"] = float(obsInfo.focus_z.to_value("mm")) 

143 if obsInfo.observation_type is not None: 

144 argDict["observationType"] = obsInfo.observation_type 

145 if obsInfo.science_program is not None: 

146 argDict["scienceProgram"] = obsInfo.science_program 

147 if obsInfo.observation_reason is not None: 

148 argDict["observationReason"] = obsInfo.observation_reason 

149 if obsInfo.object is not None: 

150 argDict["object"] = obsInfo.object 

151 if obsInfo.has_simulated_content is not None: 

152 argDict["hasSimulatedContent"] = obsInfo.has_simulated_content 

153 

154 # VisitInfo uses the middle of the observation for the date 

155 if obsInfo.datetime_begin is not None and obsInfo.datetime_end is not None: 

156 tdelta = obsInfo.datetime_end - obsInfo.datetime_begin 

157 middle = obsInfo.datetime_begin + 0.5 * tdelta 

158 

159 # DateTime uses nanosecond resolution, regardless of the resolution 

160 # of the original date 

161 middle.precision = 9 

162 # isot is ISO8601 format with "T" separating date and time and no 

163 # time zone 

164 argDict["date"] = DateTime(middle.tai.isot, DateTime.TAI) 

165 

166 # Derive earth rotation angle from UT1 (being out by a second is 

167 # not a big deal given the uncertainty over exactly what part of 

168 # the observation we are needing it for). 

169 # ERFA needs a UT1 time split into two floats 

170 # We ignore any problems with DUT1 not being defined for now. 

171 try: 

172 # Catch any warnings about the time being in the future 

173 # since there is nothing we can do about that for simulated 

174 # data and it tells us nothing for data from the past. 

175 with warnings.catch_warnings(): 

176 # If we are using the real erfa it is not an AstropyWarning 

177 # During transition period filter both 

178 warnings.simplefilter("ignore", category=astropy.utils.exceptions.AstropyWarning) 

179 if ErfaWarning is not None: 

180 warnings.simplefilter("ignore", category=ErfaWarning) 

181 ut1time = middle.ut1 

182 except iers.IERSRangeError: 

183 ut1time = middle 

184 

185 era = erfa.era00(ut1time.jd1, ut1time.jd2) 

186 argDict["era"] = era * radians 

187 else: 

188 argDict["date"] = DateTime() 

189 

190 # Coordinates 

191 if obsInfo.tracking_radec is not None: 

192 icrs = obsInfo.tracking_radec.transform_to("icrs") 

193 argDict["boresightRaDec"] = SpherePoint(icrs.ra.degree, icrs.dec.degree, units=degrees) 

194 

195 altaz = obsInfo.altaz_begin 

196 if altaz is not None: 

197 argDict["boresightAzAlt"] = SpherePoint(altaz.az.degree, altaz.alt.degree, units=degrees) 

198 

199 argDict["boresightAirmass"] = obsInfo.boresight_airmass 

200 

201 if obsInfo.boresight_rotation_angle is not None: 

202 argDict["boresightRotAngle"] = obsInfo.boresight_rotation_angle.degree * degrees 

203 

204 if obsInfo.boresight_rotation_coord is not None: 

205 rotType = RotType.UNKNOWN 

206 if obsInfo.boresight_rotation_coord == "sky": 

207 rotType = RotType.SKY 

208 argDict["rotType"] = rotType 

209 

210 # Weather and Observatory Location 

211 temperature = float("nan") 

212 if obsInfo.temperature is not None: 

213 temperature = float(obsInfo.temperature.to_value("deg_C", astropy.units.temperature())) 

214 pressure = float("nan") 

215 if obsInfo.pressure is not None: 

216 pressure = float(obsInfo.pressure.to_value("Pa")) 

217 relative_humidity = float("nan") 

218 if obsInfo.relative_humidity is not None: 

219 relative_humidity = obsInfo.relative_humidity 

220 argDict["weather"] = Weather(temperature, pressure, relative_humidity) 

221 

222 if obsInfo.location is not None: 

223 geolocation = obsInfo.location.to_geodetic() 

224 argDict["observatory"] = Observatory( 

225 geolocation.lon.degree * degrees, 

226 geolocation.lat.degree * degrees, 

227 float(geolocation.height.to_value("m")), 

228 ) 

229 

230 for key in list(argDict.keys()): # use a copy because we may delete items 

231 if argDict[key] is None: 

232 if log is not None: 

233 log.warning("argDict[%s] is None; stripping", key) 

234 del argDict[key] 

235 

236 return VisitInfo(**argDict)