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 

20 

21from astro_metadata_translator import ObservationInfo 

22 

23# PropertyList is optional 

24try: 

25 import lsst.daf.base as daf_base 

26except ImportError: 

27 daf_base = None 

28 

29 

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

31try: 

32 Loader = yaml.FullLoader 

33except AttributeError: 

34 Loader = yaml.Loader 

35 

36 

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

38# we can use if daf_base is not available. 

39def pl_constructor(loader, node): 

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

41 pl = OrderedDict() 

42 yield pl 

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

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

45 if dtype == "Double": 

46 pl[key] = float(value) 

47 elif dtype == "Int": 

48 pl[key] = int(value) 

49 elif dtype == "Bool": 

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

51 else: 

52 pl[key] = value 

53 

54 

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

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

57 

58 

59def read_test_file(filename, dir=None): 

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

61 

62 Parameters 

63 ---------- 

64 filename : `str` 

65 Name of file in the data directory. 

66 dir : `str`, optional. 

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

68 specified. 

69 

70 Returns 

71 ------- 

72 header : `dict`-like 

73 Header read from file. 

74 """ 

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

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

77 with open(filename) as fd: 

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

79 return header 

80 

81 

82class MetadataAssertHelper: 

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

84 translations. 

85 """ 

86 

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

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

89 

90 Parameters 

91 ---------- 

92 obsinfo : `ObservationInfo` 

93 Object to check. 

94 max_sep : `float`, optional 

95 Maximum separation between AltAz derived from RA/Dec headers 

96 and that found in the AltAz headers. 

97 amdelta : `float`, optional 

98 Max difference between header airmass and derived airmass. 

99 

100 Raises 

101 ------ 

102 AssertionError 

103 Inconsistencies found. 

104 """ 

105 self.assertIsNotNone(obsinfo.tracking_radec) 

106 self.assertIsNotNone(obsinfo.altaz_begin) 

107 

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

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

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

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

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

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

114 

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

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

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

118 

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

120 wcs_params=None, **kwargs): 

121 """Check contents of an ObservationInfo. 

122 

123 Parameters 

124 ---------- 

125 file : `str` 

126 Path to YAML file representing the header. 

127 dir : `str`, optional 

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

129 check_wcs : `bool`, optional 

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

131 wcs_params : `dict`, optional 

132 Parameters to pass to `assertCoordinatesConsistent`. 

133 kwargs : `dict` 

134 Keys matching `ObservationInfo` properties with values 

135 to be tested. 

136 

137 Raises 

138 ------ 

139 AssertionError 

140 A value in the ObservationInfo derived from the file is 

141 inconsistent. 

142 """ 

143 header = read_test_file(file, dir=dir) 

144 self.assertObservationInfo(header, check_wcs=check_wcs, wcs_params=wcs_params, **kwargs) 

145 

146 def assertObservationInfo(self, header, check_wcs=True, wcs_params=None, **kwargs): # noqa: N802 

147 """Check contents of an ObservationInfo. 

148 

149 Parameters 

150 ---------- 

151 header : `dict`-like 

152 Header to be checked. 

153 check_wcs : `bool`, optional 

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

155 are automatically disabled if the translated header does 

156 not appear to be "science". 

157 wcs_params : `dict`, optional 

158 Parameters to pass to `assertCoordinatesConsistent`. 

159 kwargs : `dict` 

160 Keys matching `ObservationInfo` properties with values 

161 to be tested. 

162 

163 Raises 

164 ------ 

165 AssertionError 

166 A value in the ObservationInfo derived from the file is 

167 inconsistent. 

168 """ 

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

170 # of all the translations 

171 obsinfo = ObservationInfo(header, pedantic=True) 

172 translator = obsinfo.translator_class_name 

173 

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

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

176 self.assertEqual(obsinfo, newinfo) 

177 

178 # Check the properties 

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

180 calculated = getattr(obsinfo, property) 

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

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

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

184 expected = expected.to_value() 

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

186 elif isinstance(calculated, u.Quantity): 

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

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

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

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

191 else: 

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

193 

194 # Date comparison error reports will benefit by specifying ISO 

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

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

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

198 # the past. 

199 def _format_date_for_testing(date): 

200 if date is not None: 

201 date.format = "isot" 

202 date.precision = 9 

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

204 return date 

205 

206 datetime_begin = _format_date_for_testing(obsinfo.datetime_begin) 

207 datetime_end = _format_date_for_testing(obsinfo.datetime_end) 

208 

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

210 self.assertLessEqual(datetime_begin, datetime_end) 

211 

212 # Check that exposure time is not outside datetime_end 

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

214 obsinfo.datetime_end) 

215 

216 # Check the WCS consistency 

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

218 if wcs_params is None: 

219 wcs_params = {} 

220 self.assertCoordinatesConsistent(obsinfo, **wcs_params)