Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 

22import warnings 

23import astropy.units 

24import astropy.utils.exceptions 

25from astropy.utils import iers 

26 

27# This is an unofficial ERFA interface provided by Astropy. 

28# We need to use this to calculate the Earth rotation angle. 

29# If Astropy change their ERFA support we will need to either bring the 

30# calculation into this package or use another python ERFA binding. 

31import astropy._erfa as erfa 

32 

33from astro_metadata_translator import ObservationInfo 

34 

35from lsst.log import Log 

36from lsst.daf.base import DateTime 

37from lsst.geom import degrees, radians 

38from lsst.afw.image import VisitInfo, RotType 

39from lsst.afw.coord import Observatory, Weather 

40from lsst.geom import SpherePoint 

41 

42__all__ = ["MakeRawVisitInfoViaObsInfo"] 

43 

44 

45class MakeRawVisitInfoViaObsInfo(object): 

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

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

48 

49 Subclasses can be used if a specific 

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

51 

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

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

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

55 user needing to add a lot of error handling. 

56 

57 Parameters 

58 ---------- 

59 log : `lsst.log.Log` or None 

60 Logger to use for messages. 

61 (None to use ``Log.getLogger("MakeRawVisitInfoViaObsInfo")``). 

62 doStripHeader : `bool`, optional 

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

64 """ 

65 

66 metadataTranslator = None 

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

68 automatic determination.""" 

69 

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

71 if log is None: 

72 log = Log.getLogger("MakeRawVisitInfoViaObsInfo") 

73 self.log = log 

74 self.doStripHeader = doStripHeader 

75 

76 def __call__(self, md, exposureId=None): 

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

78 

79 Parameters 

80 ---------- 

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

82 Metadata to pull from. 

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

84 exposureId : `int`, optional 

85 Ignored. Here for compatibility with `MakeRawVisitInfo`. 

86 

87 Returns 

88 ------- 

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

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

91 a `~astro_metadata_translator.MetadataTranslator`. 

92 """ 

93 

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

95 

96 if self.doStripHeader: 

97 # Strip all the cards out that were used 

98 for c in obsInfo.cards_used: 

99 del md[c] 

100 

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

102 

103 @staticmethod 

104 def observationInfo2visitInfo(obsInfo, log=None): 

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

106 `~astro_metadata_translator.ObservationInfo` 

107 

108 Parameters 

109 ---------- 

110 obsInfo : `astro_metadata_translator.ObservationInfo` 

111 Information gathered from the observation metadata. 

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

113 Logger to use for logging informational messages. 

114 If `None` logging will be disabled. 

115 

116 Returns 

117 ------- 

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

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

120 `~astro_metadata_translator.ObservationInfo`. 

121 """ 

122 argDict = dict() 

123 

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

125 if obsInfo.exposure_time is not None: 

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

127 if obsInfo.dark_time is not None: 

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

129 argDict["exposureId"] = obsInfo.detector_exposure_id 

130 

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

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

133 tdelta = obsInfo.datetime_end - obsInfo.datetime_begin 

134 middle = obsInfo.datetime_begin + 0.5*tdelta 

135 

136 # DateTime uses nanosecond resolution, regardless of the resolution 

137 # of the original date 

138 middle.precision = 9 

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

140 # time zone 

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

142 

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

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

145 # the observation we are needing it for). 

146 # ERFA needs a UT1 time split into two floats 

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

148 try: 

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

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

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

152 with warnings.catch_warnings(): 

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

154 ut1time = middle.ut1 

155 except iers.IERSRangeError: 

156 ut1time = middle 

157 

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

159 argDict["era"] = era * radians 

160 else: 

161 argDict["date"] = DateTime() 

162 

163 # Coordinates 

164 if obsInfo.tracking_radec is not None: 

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

166 argDict["boresightRaDec"] = SpherePoint(icrs.ra.degree, 

167 icrs.dec.degree, units=degrees) 

168 

169 altaz = obsInfo.altaz_begin 

170 if altaz is not None: 

171 argDict["boresightAzAlt"] = SpherePoint(altaz.az.degree, 

172 altaz.alt.degree, units=degrees) 

173 

174 argDict["boresightAirmass"] = obsInfo.boresight_airmass 

175 

176 if obsInfo.boresight_rotation_angle is not None: 

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

178 

179 if obsInfo.boresight_rotation_coord is not None: 

180 rotType = RotType.UNKNOWN 

181 if obsInfo.boresight_rotation_coord == "sky": 

182 rotType = RotType.SKY 

183 argDict["rotType"] = rotType 

184 

185 # Weather and Observatory Location 

186 temperature = float("nan") 

187 if obsInfo.temperature is not None: 

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

189 pressure = float("nan") 

190 if obsInfo.pressure is not None: 

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

192 relative_humidity = float("nan") 

193 if obsInfo.relative_humidity is not None: 

194 relative_humidity = obsInfo.relative_humidity 

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

196 

197 if obsInfo.location is not None: 

198 geolocation = obsInfo.location.to_geodetic() 

199 argDict["observatory"] = Observatory(geolocation.lon.degree*degrees, 

200 geolocation.lat.degree*degrees, 

201 geolocation.height.to_value("m")) 

202 

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

204 if argDict[key] is None: 

205 if log is not None: 

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

207 del argDict[key] 

208 

209 return VisitInfo(**argDict)