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

109 statements  

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

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, VisitInfoTranslator 

45 

46from lsst.afw.coord import Observatory, Weather 

47from lsst.afw.image import RotType, VisitInfo, setVisitInfoMetadata 

48from lsst.daf.base import DateTime, PropertyList 

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) 

237 

238 @staticmethod 

239 def visitInfo2observationInfo(visitInfo: lsst.afw.image.VisitInfo) -> ObservationInfo: 

240 """Construct a `~astro_metadata_translator.ObservationInfo` from a 

241 `~lsst.afw.image.VisitInfo`. 

242 

243 Parameters 

244 ---------- 

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

246 The object to convert. 

247 

248 Returns 

249 ------- 

250 observationInfo : `~astro_metadata_translator.ObservationInfo` 

251 New `~astro_metadata_translator.ObservationInfo` derived from 

252 the supplied `~lsst.afw.image.VisitInfo`. 

253 

254 Notes 

255 ----- 

256 Since a `~lsst.afw.image.VisitInfo` has less information than a 

257 `~astro_metadata_translator.ObservationInfo`, many fields in the 

258 returned object will be `None`. 

259 """ 

260 pl = PropertyList() 

261 setVisitInfoMetadata(pl, visitInfo) 

262 

263 # Try the given header looking for VisitInfo hints. 

264 # We get lots of warnings if nothing can be found. Currently 

265 # no way to disable those without capturing them. 

266 obs_info = ObservationInfo.from_header(pl, translator_class=VisitInfoTranslator, quiet=True) 

267 return obs_info