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 astro_metadata_translator. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

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

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

7# for details of code ownership. 

8# 

9# Use of this source code is governed by a 3-clause BSD-style 

10# license that can be found in the LICENSE file. 

11 

12__all__ = ("read_test_file", "MetadataAssertHelper") 

13 

14import astropy.units as u 

15import os 

16import pickle 

17import yaml 

18from collections import OrderedDict 

19from astropy.time import Time 

20import astropy.utils.exceptions 

21import warnings 

22 

23from astro_metadata_translator import ObservationInfo 

24 

25# PropertyList is optional 

26try: 

27 import lsst.daf.base as daf_base 

28except ImportError: 

29 daf_base = None 

30 

31 

32# For YAML >= 5.1 need a different Loader for the constructor 

33try: 

34 Loader = yaml.FullLoader 

35except AttributeError: 

36 Loader = yaml.Loader 

37 

38 

39# Define a YAML loader for lsst.daf.base.PropertySet serializations that 

40# we can use if daf_base is not available. 

41def pl_constructor(loader, node): 

42 """Construct an OrderedDict from a YAML file containing a PropertyList.""" 

43 pl = OrderedDict() 

44 yield pl 

45 state = loader.construct_sequence(node, deep=True) 

46 for key, dtype, value, comment in state: 

47 if dtype == "Double": 

48 pl[key] = float(value) 

49 elif dtype == "Int": 

50 pl[key] = int(value) 

51 elif dtype == "Bool": 

52 pl[key] = True if value == "true" else False 

53 else: 

54 pl[key] = value 

55 

56 

57if daf_base is None: 57 ↛ 58line 57 didn't jump to line 58, because the condition on line 57 was never true

58 yaml.add_constructor("lsst.daf.base.PropertyList", pl_constructor, Loader=Loader) 

59 

60 

61def read_test_file(filename, dir=None): 

62 """Read the named test file relative to the location of this helper 

63 

64 Parameters 

65 ---------- 

66 filename : `str` 

67 Name of file in the data directory. 

68 dir : `str`, optional. 

69 Directory from which to read file. Current directory used if none 

70 specified. 

71 

72 Returns 

73 ------- 

74 header : `dict`-like 

75 Header read from file. 

76 """ 

77 if dir is not None and not os.path.isabs(filename): 

78 filename = os.path.join(dir, filename) 

79 with open(filename) as fd: 

80 header = yaml.load(fd, Loader=Loader) 

81 # Cannot directly check for Mapping because PropertyList is not one 

82 if not hasattr(header, "items"): 

83 raise ValueError(f"Contents of YAML file {filename} are not a mapping, they are {type(header)}") 

84 return header 

85 

86 

87class MetadataAssertHelper: 

88 """Class with helpful asserts that can be used for testing metadata 

89 translations. 

90 """ 

91 

92 def assertCoordinatesConsistent(self, obsinfo, max_sep=1.0, amdelta=0.01): # noqa: N802 

93 """Check that SkyCoord, AltAz, and airmass are self consistent. 

94 

95 Parameters 

96 ---------- 

97 obsinfo : `ObservationInfo` 

98 Object to check. 

99 max_sep : `float`, optional 

100 Maximum separation between AltAz derived from RA/Dec headers 

101 and that found in the AltAz headers. 

102 amdelta : `float`, optional 

103 Max difference between header airmass and derived airmass. 

104 

105 Raises 

106 ------ 

107 AssertionError 

108 Inconsistencies found. 

109 """ 

110 self.assertIsNotNone(obsinfo.tracking_radec) 

111 self.assertIsNotNone(obsinfo.altaz_begin) 

112 

113 # Is airmass from header close to airmass from AltAz headers? 

114 # In most cases there is uncertainty over whether the elevation 

115 # and airmass in the header are for the start, end, or middle 

116 # of the observation. Sometimes the AltAz is from the start 

117 # but the airmass is from the middle so accuracy is not certain. 

118 self.assertAlmostEqual(obsinfo.altaz_begin.secz.to_value(), obsinfo.boresight_airmass, delta=amdelta) 

119 

120 # Is AltAz from headers close to AltAz from RA/Dec headers? 

121 # Can trigger warnings from Astropy if date is in future 

122 with warnings.catch_warnings(): 

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

124 sep = obsinfo.altaz_begin.separation(obsinfo.tracking_radec.altaz) 

125 self.assertLess(sep.to_value(unit="arcmin"), max_sep, msg="AltAz inconsistent with RA/Dec") 

126 

127 def assertObservationInfoFromYaml(self, file, dir=None, check_wcs=True, # noqa: N802 

128 wcs_params=None, **kwargs): 

129 """Check contents of an ObservationInfo. 

130 

131 Parameters 

132 ---------- 

133 file : `str` 

134 Path to YAML file representing the header. 

135 dir : `str`, optional 

136 Optional directory from which to read ``file``. 

137 check_wcs : `bool`, optional 

138 Check the consistency of the RA/Dec and AltAz values. 

139 wcs_params : `dict`, optional 

140 Parameters to pass to `assertCoordinatesConsistent`. 

141 kwargs : `dict` 

142 Keys matching `ObservationInfo` properties with values 

143 to be tested. 

144 

145 Raises 

146 ------ 

147 AssertionError 

148 A value in the ObservationInfo derived from the file is 

149 inconsistent. 

150 """ 

151 header = read_test_file(file, dir=dir) 

152 self.assertObservationInfo(header, filename=file, check_wcs=check_wcs, 

153 wcs_params=wcs_params, **kwargs) 

154 

155 def assertObservationInfo(self, header, filename=None, check_wcs=True, # noqa: N802 

156 wcs_params=None, **kwargs): 

157 """Check contents of an ObservationInfo. 

158 

159 Parameters 

160 ---------- 

161 header : `dict`-like 

162 Header to be checked. 

163 filename : `str`, optional 

164 Name of the filename associated with this header if known. 

165 check_wcs : `bool`, optional 

166 Check the consistency of the RA/Dec and AltAz values. Checks 

167 are automatically disabled if the translated header does 

168 not appear to be "science". 

169 wcs_params : `dict`, optional 

170 Parameters to pass to `assertCoordinatesConsistent`. 

171 kwargs : `dict` 

172 Keys matching `ObservationInfo` properties with values 

173 to be tested. 

174 

175 Raises 

176 ------ 

177 AssertionError 

178 A value in the ObservationInfo derived from the file is 

179 inconsistent. 

180 """ 

181 # For testing we force pedantic mode since we are in charge 

182 # of all the translations 

183 obsinfo = ObservationInfo(header, pedantic=True, filename=filename) 

184 translator = obsinfo.translator_class_name 

185 

186 # Check that we can pickle and get back the same properties 

187 newinfo = pickle.loads(pickle.dumps(obsinfo)) 

188 self.assertEqual(obsinfo, newinfo) 

189 

190 # Check the properties 

191 for property, expected in kwargs.items(): 

192 calculated = getattr(obsinfo, property) 

193 msg = f"Comparing property {property} using translator {translator}" 

194 if isinstance(expected, u.Quantity) and calculated is not None: 

195 calculated = calculated.to_value(unit=expected.unit) 

196 expected = expected.to_value() 

197 self.assertAlmostEqual(calculated, expected, msg=msg) 

198 elif isinstance(calculated, u.Quantity): 

199 # Only happens if the test is not a quantity when it should be 

200 self.fail(f"Expected {expected!r} but got Quantity '{calculated}': {msg}") 

201 elif isinstance(expected, float) and calculated is not None: 

202 self.assertAlmostEqual(calculated, expected, msg=msg) 

203 else: 

204 self.assertEqual(calculated, expected, msg=msg) 

205 

206 # Date comparison error reports will benefit by specifying ISO 

207 # format. Generate a new Time object at fixed precision 

208 # to work around the fact that (as of astropy 3.1) adding 0.0 seconds 

209 # to a Time results in a new Time object that is a few picoseconds in 

210 # the past. 

211 def _format_date_for_testing(date): 

212 if date is not None: 

213 date.format = "isot" 

214 date.precision = 9 

215 date = Time(str(date), scale=date.scale, format="isot") 

216 return date 

217 

218 datetime_begin = _format_date_for_testing(obsinfo.datetime_begin) 

219 datetime_end = _format_date_for_testing(obsinfo.datetime_end) 

220 

221 # Check that dates are defined and end is the same or after beginning 

222 self.assertLessEqual(datetime_begin, datetime_end) 

223 

224 # Check that exposure time is not outside datetime_end 

225 self.assertLessEqual(obsinfo.datetime_begin + obsinfo.exposure_time, 

226 obsinfo.datetime_end) 

227 

228 # Check the WCS consistency 

229 if check_wcs and obsinfo.observation_type == "science": 

230 if wcs_params is None: 

231 wcs_params = {} 

232 self.assertCoordinatesConsistent(obsinfo, **wcs_params)