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

103 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-27 01:24 -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. 

72 (None to use 

73 ``Log.getLogger("lsst.obs.base.makeRawVisitInfoViaObsInfo")``). 

74 doStripHeader : `bool`, optional 

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

76 """ 

77 

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

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

80 automatic determination.""" 

81 

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

83 if log is None: 

84 log = logging.getLogger(__name__) 

85 self.log = log 

86 self.doStripHeader = doStripHeader 

87 

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

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

90 

91 Parameters 

92 ---------- 

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

94 Metadata to pull from. 

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

96 

97 Returns 

98 ------- 

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

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

101 a `~astro_metadata_translator.MetadataTranslator`. 

102 """ 

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

104 

105 if self.doStripHeader: 

106 # Strip all the cards out that were used 

107 for c in obsInfo.cards_used: 

108 del md[c] 

109 

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

111 

112 @staticmethod 

113 def observationInfo2visitInfo( 

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

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

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

117 `~astro_metadata_translator.ObservationInfo`. 

118 

119 Parameters 

120 ---------- 

121 obsInfo : `astro_metadata_translator.ObservationInfo` 

122 Information gathered from the observation metadata. 

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

124 Logger to use for logging informational messages. 

125 If `None` logging will be disabled. 

126 

127 Returns 

128 ------- 

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

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

131 `~astro_metadata_translator.ObservationInfo`. 

132 """ 

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

134 

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

136 if obsInfo.exposure_time is not None: 

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

138 if obsInfo.dark_time is not None: 

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

140 argDict["id"] = obsInfo.exposure_id 

141 argDict["instrumentLabel"] = obsInfo.instrument 

142 if obsInfo.focus_z is not None: 

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

144 if obsInfo.observation_type is not None: 

145 argDict["observationType"] = obsInfo.observation_type 

146 if obsInfo.science_program is not None: 

147 argDict["scienceProgram"] = obsInfo.science_program 

148 if obsInfo.observation_reason is not None: 

149 argDict["observationReason"] = obsInfo.observation_reason 

150 if obsInfo.object is not None: 

151 argDict["object"] = obsInfo.object 

152 if obsInfo.has_simulated_content is not None: 

153 argDict["hasSimulatedContent"] = obsInfo.has_simulated_content 

154 

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

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

157 tdelta = obsInfo.datetime_end - obsInfo.datetime_begin 

158 middle = obsInfo.datetime_begin + 0.5 * tdelta 

159 

160 # DateTime uses nanosecond resolution, regardless of the resolution 

161 # of the original date 

162 middle.precision = 9 

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

164 # time zone 

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

166 

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

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

169 # the observation we are needing it for). 

170 # ERFA needs a UT1 time split into two floats 

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

172 try: 

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

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

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

176 with warnings.catch_warnings(): 

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

178 # During transition period filter both 

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

180 if ErfaWarning is not None: 

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

182 ut1time = middle.ut1 

183 except iers.IERSRangeError: 

184 ut1time = middle 

185 

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

187 argDict["era"] = era * radians 

188 else: 

189 argDict["date"] = DateTime() 

190 

191 # Coordinates 

192 if obsInfo.tracking_radec is not None: 

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

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

195 

196 altaz = obsInfo.altaz_begin 

197 if altaz is not None: 

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

199 

200 argDict["boresightAirmass"] = obsInfo.boresight_airmass 

201 

202 if obsInfo.boresight_rotation_angle is not None: 

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

204 

205 if obsInfo.boresight_rotation_coord is not None: 

206 rotType = RotType.UNKNOWN 

207 if obsInfo.boresight_rotation_coord == "sky": 

208 rotType = RotType.SKY 

209 argDict["rotType"] = rotType 

210 

211 # Weather and Observatory Location 

212 temperature = float("nan") 

213 if obsInfo.temperature is not None: 

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

215 pressure = float("nan") 

216 if obsInfo.pressure is not None: 

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

218 relative_humidity = float("nan") 

219 if obsInfo.relative_humidity is not None: 

220 relative_humidity = obsInfo.relative_humidity 

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

222 

223 if obsInfo.location is not None: 

224 geolocation = obsInfo.location.to_geodetic() 

225 argDict["observatory"] = Observatory( 

226 geolocation.lon.degree * degrees, 

227 geolocation.lat.degree * degrees, 

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

229 ) 

230 

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

232 if argDict[key] is None: 

233 if log is not None: 

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

235 del argDict[key] 

236 

237 return VisitInfo(**argDict)