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