Coverage for tests / test_translation.py: 14%
137 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-28 08:38 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-28 08:38 +0000
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.
12import logging
13import os.path
14import unittest
16from astropy.time import Time, TimeDelta
18from astro_metadata_translator import FitsTranslator, ObservationInfo, StubTranslator
20TESTDIR = os.path.abspath(os.path.dirname(__file__))
23class InstrumentTestTranslator(FitsTranslator, StubTranslator):
24 """Simple FITS-like translator to test the infrastructure."""
26 # Needs a name to be registered
27 name = "TestTranslator"
29 # Indicate the instrument this class understands
30 supported_instrument = "SCUBA_test"
32 # Some new mappings, including an override
33 _trivial_map = {
34 "telescope": "TELCODE",
35 "exposure_id": "EXPID",
36 "relative_humidity": "HUMIDITY",
37 "detector_name": "DETNAME",
38 "observation_id": "OBSID",
39 }
41 # Add translator method to test joining
42 def to_physical_filter(self) -> str:
43 return self._join_keyword_values(["DETNAME", "HUMIDITY"], delim="_")
46class MissingMethodsTranslator(FitsTranslator):
47 """Translator class that does not implement all the methods."""
49 pass
52class TranslatorTestCase(unittest.TestCase):
53 """Test core translation infrastructure."""
55 def setUp(self) -> None:
56 # Known simple header
57 self.header = {
58 "TELESCOP": "JCMT",
59 "TELCODE": "LSST",
60 "INSTRUME": "SCUBA_test",
61 "DATE-OBS": "2000-01-01T01:00:01.500",
62 "DATE-END": "2000-01-01T02:00:01.500",
63 "OBSGEO-X": "-5464588.84421314",
64 "OBSGEO-Y": "-2493000.19137644",
65 "OBSGEO-Z": "2150653.35350771",
66 "OBSID": "20000101_00002",
67 "EXPID": "22", # Should cast to a number
68 "DETNAME": 76, # Should cast to a string
69 "HUMIDITY": "55", # Should cast to a float
70 "BAZ": "bar",
71 }
73 def test_manual_translation(self) -> None:
74 header = self.header
75 translator = FitsTranslator(header)
77 # Treat the header as standard FITS
78 self.assertFalse(FitsTranslator.can_translate(header))
79 self.assertEqual(translator.to_telescope(), "JCMT")
80 self.assertEqual(translator.to_instrument(), "SCUBA_test")
81 self.assertEqual(translator.to_datetime_begin(), Time(header["DATE-OBS"], format="isot"))
83 # This class will issue warnings
84 with self.assertLogs("astro_metadata_translator") as cm:
86 class InstrumentTestTranslatorExtras(InstrumentTestTranslator):
87 """Version of InstrumentTestTranslator with unexpected
88 fields.
89 """
91 name = "InstrumentTestTranslatorExtras"
92 _trivial_map = {"foobar": "BAZ"}
93 _const_map = {"format": "HDF5"}
95 self.assertIn("Unexpected trivial", cm.output[0])
96 self.assertIn("Unexpected constant", cm.output[1])
98 # Use the special test translator instead
99 translator = InstrumentTestTranslatorExtras(header)
100 self.assertTrue(InstrumentTestTranslator.can_translate(header))
101 self.assertEqual(translator.to_telescope(), "LSST")
102 self.assertEqual(translator.to_instrument(), "SCUBA_test")
103 self.assertEqual(translator.to_format(), "HDF5")
104 self.assertEqual(translator.to_foobar(), "bar")
106 def test_translator(self) -> None:
107 header = self.header
109 # Specify a translation class
110 with self.assertWarns(UserWarning):
111 # Since the translator is incomplete it should issue warnings
112 v1 = ObservationInfo(header, translator_class=InstrumentTestTranslator)
113 self.assertEqual(v1.instrument, "SCUBA_test")
114 self.assertEqual(v1.telescope, "LSST")
115 self.assertEqual(v1.exposure_id, 22)
116 self.assertIsInstance(v1.exposure_id, int)
117 self.assertEqual(v1.detector_name, "76")
118 self.assertEqual(v1.relative_humidity, 55.0)
119 self.assertIsInstance(v1.relative_humidity, float)
120 self.assertEqual(v1.physical_filter, "76_55")
122 # Now automated class
123 with self.assertWarns(UserWarning):
124 # Since the translator is incomplete it should issue warnings
125 v1 = ObservationInfo(header)
126 self.assertEqual(v1.instrument, "SCUBA_test")
127 self.assertEqual(v1.telescope, "LSST")
129 location = v1.location.to_geodetic()
130 self.assertAlmostEqual(float(location.height.to("m").to_value()), 4123.0, places=1)
132 # Check that headers have been removed
133 new_hdr = v1.stripped_header()
134 self.assertNotIn("INSTRUME", new_hdr)
135 self.assertNotIn("OBSGEO-X", new_hdr)
136 self.assertIn("TELESCOP", new_hdr)
138 # Check the list of cards that were used
139 used = v1.cards_used
140 self.assertIn("INSTRUME", used)
141 self.assertIn("OBSGEO-Y", used)
142 self.assertNotIn("TELESCOP", used)
144 # Stringification
145 summary = str(v1)
146 self.assertIn("datetime_begin", summary)
148 # Create with a subset of properties
149 v2 = ObservationInfo(
150 header,
151 translator_class=InstrumentTestTranslator,
152 subset={"telescope", "datetime_begin", "exposure_group"},
153 )
155 self.assertEqual(v2.telescope, v1.telescope)
156 self.assertEqual(v2.datetime_begin, v2.datetime_begin)
157 self.assertIsNone(v2.datetime_end)
158 self.assertIsNone(v2.location)
159 self.assertIsNone(v2.observation_id)
161 # Use the new, explicit constructor.
162 v3 = ObservationInfo.from_header(
163 header,
164 translator_class=InstrumentTestTranslator,
165 subset={"telescope", "datetime_begin", "exposure_group"},
166 )
167 self.assertEqual(v3, v2)
169 with self.assertRaises(TypeError):
170 # This fails a not-a-class check.
171 ObservationInfo(header, translator_class={})
172 with self.assertRaises(TypeError):
173 # Check that the translator class is checked.
174 ObservationInfo(header, translator_class=dict)
175 with self.assertRaises(ValueError):
176 ObservationInfo(header, keyword="unknown")
177 with self.assertRaises(ValueError):
178 ObservationInfo.from_header(header, subset=set())
180 def test_corrections(self) -> None:
181 """Apply corrections before translation."""
182 header = self.header
184 # Specify a translation class
185 with self.assertWarns(UserWarning):
186 # Since the translator is incomplete it should issue warnings
187 v1 = ObservationInfo(
188 header,
189 translator_class=InstrumentTestTranslator,
190 search_path=[os.path.join(TESTDIR, "data", "corrections")],
191 )
193 # These values should match the expected translation
194 self.assertEqual(v1.instrument, "SCUBA_test")
195 self.assertEqual(v1.detector_name, "76")
196 self.assertEqual(v1.relative_humidity, 55.0)
197 self.assertIsInstance(v1.relative_humidity, float)
198 self.assertEqual(v1.physical_filter, "76_55")
200 # These two should be the "corrected" values
201 self.assertEqual(v1.telescope, "AuxTel")
202 self.assertEqual(v1.exposure_id, 42)
204 def test_failures(self) -> None:
205 header = {}
207 with self.assertRaises(TypeError):
208 ObservationInfo(header, translator_class=ObservationInfo)
210 with self.assertRaises(ValueError):
211 ObservationInfo(
212 header, translator_class=InstrumentTestTranslator, subset={"definitely_not_known"}
213 )
215 with self.assertRaises(ValueError):
216 ObservationInfo(
217 header, translator_class=InstrumentTestTranslator, required={"definitely_not_known"}
218 )
220 with self.assertLogs("astro_metadata_translator"):
221 with self.assertWarns(UserWarning):
222 ObservationInfo(header, translator_class=InstrumentTestTranslator, pedantic=False)
224 with self.assertLogs("astro_metadata_translator", level=logging.DEBUG) as cm:
225 with self.assertWarns(UserWarning):
226 ObservationInfo(header, translator_class=InstrumentTestTranslator, pedantic=False, quiet=True)
227 # Check that at least one of those DEBUG logs was a translation
228 # warning.
229 self.assertIn("DEBUG:astro_metadata_translator.observationInfo:Calculation of property", cm.output[0])
230 self.assertIn(
231 "DEBUG:astro_metadata_translator.observationInfo:Ignoring Error calculating property",
232 cm.output[1],
233 )
235 with self.assertRaises(KeyError):
236 with self.assertWarns(UserWarning):
237 ObservationInfo(header, translator_class=InstrumentTestTranslator, pedantic=True)
239 # Pass in header where the key does exist but the value is bad.
240 bad_header = {
241 "OBSGEO-X": "not-float",
242 "OBSGEO-Y": "not-float",
243 "OBSGEO-Z": "not-float",
244 "TELESCOP": "JCMT",
245 "TELCODE": "LSST",
246 "INSTRUME": "SCUBA_test",
247 }
249 with self.assertLogs("astro_metadata_translator"):
250 with self.assertWarns(UserWarning):
251 ObservationInfo(bad_header, translator_class=InstrumentTestTranslator, pedantic=False)
253 with self.assertLogs("astro_metadata_translator"):
254 with self.assertWarns(UserWarning):
255 ObservationInfo(
256 header, translator_class=InstrumentTestTranslator, pedantic=False, filename="testfile1"
257 )
259 with self.assertRaises(KeyError):
260 with self.assertWarns(UserWarning):
261 ObservationInfo(
262 header, translator_class=InstrumentTestTranslator, pedantic=True, filename="testfile2"
263 )
265 with self.assertRaises(NotImplementedError):
266 with self.assertLogs("astro_metadata_translator", level="WARN"):
267 ObservationInfo(header, translator_class=MissingMethodsTranslator)
269 with self.assertRaises(KeyError):
270 with self.assertWarns(UserWarning):
271 with self.assertLogs("astro_metadata_translator", level="WARN"):
272 ObservationInfo(
273 header,
274 translator_class=InstrumentTestTranslator,
275 pedantic=False,
276 required={"boresight_airmass"},
277 )
279 def test_observing_date(self) -> None:
280 """Test the observing_date class methods."""
281 date1 = Time("2023-07-06T13:00", format="isot", scale="tai")
282 day_obs = StubTranslator.observing_date_to_observing_day(date1, 12 * 3600)
283 self.assertEqual(day_obs, 20230706)
285 date2 = Time("2023-07-07T11:00", format="isot", scale="tai")
286 day_obs = StubTranslator.observing_date_to_observing_day(date2, TimeDelta("0.5d", scale="tai"))
287 self.assertEqual(day_obs, 20230706)
288 day_obs = StubTranslator.observing_date_to_observing_day(date2, 1.0)
289 self.assertEqual(day_obs, 20230707)
292if __name__ == "__main__":
293 unittest.main()