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

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
20import astropy.utils.exceptions
21import warnings
23from astro_metadata_translator import ObservationInfo
25# PropertyList is optional
26try:
27 import lsst.daf.base as daf_base
28except ImportError:
29 daf_base = None
32# For YAML >= 5.1 need a different Loader for the constructor
33try:
34 Loader = yaml.FullLoader
35except AttributeError:
36 Loader = yaml.Loader
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
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)
61def read_test_file(filename, dir=None):
62 """Read the named test file relative to the location of this helper
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.
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
87class MetadataAssertHelper:
88 """Class with helpful asserts that can be used for testing metadata
89 translations.
90 """
92 def assertCoordinatesConsistent(self, obsinfo, max_sep=1.0, amdelta=0.01): # noqa: N802
93 """Check that SkyCoord, AltAz, and airmass are self consistent.
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.
105 Raises
106 ------
107 AssertionError
108 Inconsistencies found.
109 """
110 self.assertIsNotNone(obsinfo.tracking_radec)
111 self.assertIsNotNone(obsinfo.altaz_begin)
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)
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")
127 def assertObservationInfoFromYaml(self, file, dir=None, check_wcs=True, # noqa: N802
128 wcs_params=None, **kwargs):
129 """Check contents of an ObservationInfo.
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.
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)
155 def assertObservationInfo(self, header, filename=None, check_wcs=True, # noqa: N802
156 wcs_params=None, **kwargs):
157 """Check contents of an ObservationInfo.
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.
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
186 # Check that we can pickle and get back the same properties
187 newinfo = pickle.loads(pickle.dumps(obsinfo))
188 self.assertEqual(obsinfo, newinfo)
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)
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
218 datetime_begin = _format_date_for_testing(obsinfo.datetime_begin)
219 datetime_end = _format_date_for_testing(obsinfo.datetime_end)
221 # Check that dates are defined and end is the same or after beginning
222 self.assertLessEqual(datetime_begin, datetime_end)
224 # Check that exposure time is not outside datetime_end
225 self.assertLessEqual(obsinfo.datetime_begin + obsinfo.exposure_time,
226 obsinfo.datetime_end)
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)