Coverage for python/astro_metadata_translator/tests.py : 19%

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.
12__all__ = ("read_test_file", "MetadataAssertHelper")
14import astropy.units as u
15import os
16import pickle
17import yaml
18from collections import OrderedDict
19from astropy.time import Time
21from astro_metadata_translator import ObservationInfo
23# PropertyList is optional
24try:
25 import lsst.daf.base as daf_base
26except ImportError:
27 daf_base = None
30# For YAML >= 5.1 need a different Loader for the constructor
31try:
32 Loader = yaml.FullLoader
33except AttributeError:
34 Loader = yaml.Loader
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
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)
59def read_test_file(filename, dir=None):
60 """Read the named test file relative to the location of this helper
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.
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 # Cannot directly check for Mapping because PropertyList is not one
80 if not hasattr(header, "items"):
81 raise ValueError(f"Contents of YAML file are not a mapping, they are {type(header)}")
82 return header
85class MetadataAssertHelper:
86 """Class with helpful asserts that can be used for testing metadata
87 translations.
88 """
90 def assertCoordinatesConsistent(self, obsinfo, max_sep=1.0, amdelta=0.01): # noqa: N802
91 """Check that SkyCoord, AltAz, and airmass are self consistent.
93 Parameters
94 ----------
95 obsinfo : `ObservationInfo`
96 Object to check.
97 max_sep : `float`, optional
98 Maximum separation between AltAz derived from RA/Dec headers
99 and that found in the AltAz headers.
100 amdelta : `float`, optional
101 Max difference between header airmass and derived airmass.
103 Raises
104 ------
105 AssertionError
106 Inconsistencies found.
107 """
108 self.assertIsNotNone(obsinfo.tracking_radec)
109 self.assertIsNotNone(obsinfo.altaz_begin)
111 # Is airmass from header close to airmass from AltAz headers?
112 # In most cases there is uncertainty over whether the elevation
113 # and airmass in the header are for the start, end, or middle
114 # of the observation. Sometimes the AltAz is from the start
115 # but the airmass is from the middle so accuracy is not certain.
116 self.assertAlmostEqual(obsinfo.altaz_begin.secz.to_value(), obsinfo.boresight_airmass, delta=amdelta)
118 # Is AltAz from headers close to AltAz from RA/Dec headers?
119 sep = obsinfo.altaz_begin.separation(obsinfo.tracking_radec.altaz)
120 self.assertLess(sep.to_value(unit="arcmin"), max_sep, msg="AltAz inconsistent with RA/Dec")
122 def assertObservationInfoFromYaml(self, file, dir=None, check_wcs=True, # noqa: N802
123 wcs_params=None, **kwargs):
124 """Check contents of an ObservationInfo.
126 Parameters
127 ----------
128 file : `str`
129 Path to YAML file representing the header.
130 dir : `str`, optional
131 Optional directory from which to read ``file``.
132 check_wcs : `bool`, optional
133 Check the consistency of the RA/Dec and AltAz values.
134 wcs_params : `dict`, optional
135 Parameters to pass to `assertCoordinatesConsistent`.
136 kwargs : `dict`
137 Keys matching `ObservationInfo` properties with values
138 to be tested.
140 Raises
141 ------
142 AssertionError
143 A value in the ObservationInfo derived from the file is
144 inconsistent.
145 """
146 header = read_test_file(file, dir=dir)
147 self.assertObservationInfo(header, filename=file, check_wcs=check_wcs,
148 wcs_params=wcs_params, **kwargs)
150 def assertObservationInfo(self, header, filename=None, check_wcs=True, # noqa: N802
151 wcs_params=None, **kwargs):
152 """Check contents of an ObservationInfo.
154 Parameters
155 ----------
156 header : `dict`-like
157 Header to be checked.
158 filename : `str`, optional
159 Name of the filename associated with this header if known.
160 check_wcs : `bool`, optional
161 Check the consistency of the RA/Dec and AltAz values. Checks
162 are automatically disabled if the translated header does
163 not appear to be "science".
164 wcs_params : `dict`, optional
165 Parameters to pass to `assertCoordinatesConsistent`.
166 kwargs : `dict`
167 Keys matching `ObservationInfo` properties with values
168 to be tested.
170 Raises
171 ------
172 AssertionError
173 A value in the ObservationInfo derived from the file is
174 inconsistent.
175 """
176 # For testing we force pedantic mode since we are in charge
177 # of all the translations
178 obsinfo = ObservationInfo(header, pedantic=True, filename=filename)
179 translator = obsinfo.translator_class_name
181 # Check that we can pickle and get back the same properties
182 newinfo = pickle.loads(pickle.dumps(obsinfo))
183 self.assertEqual(obsinfo, newinfo)
185 # Check the properties
186 for property, expected in kwargs.items():
187 calculated = getattr(obsinfo, property)
188 msg = f"Comparing property {property} using translator {translator}"
189 if isinstance(expected, u.Quantity) and calculated is not None:
190 calculated = calculated.to_value(unit=expected.unit)
191 expected = expected.to_value()
192 self.assertAlmostEqual(calculated, expected, msg=msg)
193 elif isinstance(calculated, u.Quantity):
194 # Only happens if the test is not a quantity when it should be
195 self.fail(f"Expected {expected!r} but got Quantity '{calculated}': {msg}")
196 elif isinstance(expected, float) and calculated is not None:
197 self.assertAlmostEqual(calculated, expected, msg=msg)
198 else:
199 self.assertEqual(calculated, expected, msg=msg)
201 # Date comparison error reports will benefit by specifying ISO
202 # format. Generate a new Time object at fixed precision
203 # to work around the fact that (as of astropy 3.1) adding 0.0 seconds
204 # to a Time results in a new Time object that is a few picoseconds in
205 # the past.
206 def _format_date_for_testing(date):
207 if date is not None:
208 date.format = "isot"
209 date.precision = 9
210 date = Time(str(date), scale=date.scale, format="isot")
211 return date
213 datetime_begin = _format_date_for_testing(obsinfo.datetime_begin)
214 datetime_end = _format_date_for_testing(obsinfo.datetime_end)
216 # Check that dates are defined and end is the same or after beginning
217 self.assertLessEqual(datetime_begin, datetime_end)
219 # Check that exposure time is not outside datetime_end
220 self.assertLessEqual(obsinfo.datetime_begin + obsinfo.exposure_time,
221 obsinfo.datetime_end)
223 # Check the WCS consistency
224 if check_wcs and obsinfo.observation_type == "science":
225 if wcs_params is None:
226 wcs_params = {}
227 self.assertCoordinatesConsistent(obsinfo, **wcs_params)