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

102 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-14 20:02 +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 

22__all__ = ["MakeRawVisitInfoViaObsInfo"] 

23 

24import logging 

25import warnings 

26from typing import ClassVar, Optional 

27 

28import astropy.units 

29import astropy.utils.exceptions 

30from astropy.utils import iers 

31 

32# Prefer the standard pyerfa over the Astropy version 

33try: 

34 import erfa 

35 

36 ErfaWarning = erfa.ErfaWarning 

37except ImportError: 

38 import astropy._erfa as erfa 

39 

40 ErfaWarning = None 

41 

42from astro_metadata_translator import MetadataTranslator, ObservationInfo 

43from lsst.afw.coord import Observatory, Weather 

44from lsst.afw.image import RotType, VisitInfo 

45from lsst.daf.base import DateTime 

46from lsst.geom import SpherePoint, degrees, radians 

47 

48 

49class MakeRawVisitInfoViaObsInfo: 

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

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

52 

53 Subclasses can be used if a specific 

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

55 

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

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

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

59 user needing to add a lot of error handling. 

60 

61 Parameters 

62 ---------- 

63 log : `logging.Logger` or None 

64 Logger to use for messages. 

65 (None to use 

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

67 doStripHeader : `bool`, optional 

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

69 """ 

70 

71 metadataTranslator: ClassVar[Optional[MetadataTranslator]] = None 

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

73 automatic determination.""" 

74 

75 def __init__(self, log=None, doStripHeader=False): 

76 if log is None: 

77 log = logging.getLogger(__name__) 

78 self.log = log 

79 self.doStripHeader = doStripHeader 

80 

81 def __call__(self, md): 

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

83 

84 Parameters 

85 ---------- 

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

87 Metadata to pull from. 

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

89 

90 Returns 

91 ------- 

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

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

94 a `~astro_metadata_translator.MetadataTranslator`. 

95 """ 

96 

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

98 

99 if self.doStripHeader: 

100 # Strip all the cards out that were used 

101 for c in obsInfo.cards_used: 

102 del md[c] 

103 

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

105 

106 @staticmethod 

107 def observationInfo2visitInfo(obsInfo, log=None): 

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

109 `~astro_metadata_translator.ObservationInfo` 

110 

111 Parameters 

112 ---------- 

113 obsInfo : `astro_metadata_translator.ObservationInfo` 

114 Information gathered from the observation metadata. 

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

116 Logger to use for logging informational messages. 

117 If `None` logging will be disabled. 

118 

119 Returns 

120 ------- 

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

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

123 `~astro_metadata_translator.ObservationInfo`. 

124 """ 

125 argDict = dict() 

126 

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

128 if obsInfo.exposure_time is not None: 

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

130 if obsInfo.dark_time is not None: 

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

132 argDict["id"] = obsInfo.exposure_id 

133 argDict["instrumentLabel"] = obsInfo.instrument 

134 if obsInfo.focus_z is not None: 

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

136 if obsInfo.observation_type is not None: 

137 argDict["observationType"] = obsInfo.observation_type 

138 if obsInfo.science_program is not None: 

139 argDict["scienceProgram"] = obsInfo.science_program 

140 if obsInfo.observation_reason is not None: 

141 argDict["observationReason"] = obsInfo.observation_reason 

142 if obsInfo.object is not None: 

143 argDict["object"] = obsInfo.object 

144 if obsInfo.has_simulated_content is not None: 

145 argDict["hasSimulatedContent"] = obsInfo.has_simulated_content 

146 

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

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

149 tdelta = obsInfo.datetime_end - obsInfo.datetime_begin 

150 middle = obsInfo.datetime_begin + 0.5 * tdelta 

151 

152 # DateTime uses nanosecond resolution, regardless of the resolution 

153 # of the original date 

154 middle.precision = 9 

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

156 # time zone 

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

158 

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

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

161 # the observation we are needing it for). 

162 # ERFA needs a UT1 time split into two floats 

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

164 try: 

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

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

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

168 with warnings.catch_warnings(): 

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

170 # During transition period filter both 

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

172 if ErfaWarning is not None: 

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

174 ut1time = middle.ut1 

175 except iers.IERSRangeError: 

176 ut1time = middle 

177 

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

179 argDict["era"] = era * radians 

180 else: 

181 argDict["date"] = DateTime() 

182 

183 # Coordinates 

184 if obsInfo.tracking_radec is not None: 

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

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

187 

188 altaz = obsInfo.altaz_begin 

189 if altaz is not None: 

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

191 

192 argDict["boresightAirmass"] = obsInfo.boresight_airmass 

193 

194 if obsInfo.boresight_rotation_angle is not None: 

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

196 

197 if obsInfo.boresight_rotation_coord is not None: 

198 rotType = RotType.UNKNOWN 

199 if obsInfo.boresight_rotation_coord == "sky": 

200 rotType = RotType.SKY 

201 argDict["rotType"] = rotType 

202 

203 # Weather and Observatory Location 

204 temperature = float("nan") 

205 if obsInfo.temperature is not None: 

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

207 pressure = float("nan") 

208 if obsInfo.pressure is not None: 

209 pressure = obsInfo.pressure.to_value("Pa") 

210 relative_humidity = float("nan") 

211 if obsInfo.relative_humidity is not None: 

212 relative_humidity = obsInfo.relative_humidity 

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

214 

215 if obsInfo.location is not None: 

216 geolocation = obsInfo.location.to_geodetic() 

217 argDict["observatory"] = Observatory( 

218 geolocation.lon.degree * degrees, 

219 geolocation.lat.degree * degrees, 

220 geolocation.height.to_value("m"), 

221 ) 

222 

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

224 if argDict[key] is None: 

225 if log is not None: 

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

227 del argDict[key] 

228 

229 return VisitInfo(**argDict)