Coverage for tests/test_basics.py: 100%
104 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-23 08:20 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-23 08:20 +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 math
13import unittest
15import astropy.time
16import astropy.units as u
17from astropy.coordinates import EarthLocation, SkyCoord
18from astropy.time import Time
19from pydantic import BaseModel, ConfigDict, ValidationError
21import astro_metadata_translator
22from astro_metadata_translator import ObservationInfo, makeObservationInfo
25class ModelWithObsInfo(BaseModel):
26 """Pydantic model that contains an ObservationInfo."""
28 model_config = ConfigDict(
29 ser_json_inf_nan="constants", # Pydantic does not preserve NaN support from child models.
30 )
32 obs_info: ObservationInfo
33 number: int
36class BasicTestCase(unittest.TestCase):
37 """Test basic metadata translation functionality."""
39 def test_basic(self) -> None:
40 version = astro_metadata_translator.__version__
41 self.assertIsNotNone(version)
43 def test_obsinfo(self) -> None:
44 """Test construction of ObservationInfo without header."""
45 obsinfo = makeObservationInfo(boresight_airmass=1.5, tracking_radec=None)
46 self.assertIsInstance(obsinfo, ObservationInfo)
47 self.assertIsNone(obsinfo.tracking_radec)
48 self.assertIsNotNone(obsinfo.boresight_airmass)
49 self.assertAlmostEqual(obsinfo.boresight_airmass, 1.5)
50 self.assertIsNone(obsinfo.observation_id)
51 self.assertEqual(obsinfo.cards_used, set())
52 self.assertEqual(obsinfo.stripped_header(), {})
54 # Check NaN equality.
55 obsinfo1 = ObservationInfo(relative_humidity=math.nan, detector_name="det1")
56 self.assertEqual(obsinfo1, obsinfo1)
57 self.assertNotEqual(ObservationInfo(observation_id="A"), ObservationInfo(observation_id="B"))
58 self.assertEqual(
59 ObservationInfo(observation_id="A") == ObservationInfo(observation_id="A", instrument="HSC"),
60 ObservationInfo(observation_id="A", instrument="HSC") == ObservationInfo(observation_id="A"),
61 )
63 with self.assertRaises(TypeError):
64 _ = obsinfo > obsinfo
66 with self.assertRaises(TypeError):
67 _ = obsinfo < obsinfo
69 with self.assertRaises(TypeError):
70 _ = obsinfo > 5
72 with self.assertRaises(TypeError):
73 _ = obsinfo < 5
75 with self.assertRaises(AttributeError):
76 obsinfo.boresight_airmass = 1.0
78 with self.assertRaises(ValidationError):
79 ObservationInfo.makeObservationInfo(boresight_airmass=1.5, observation_id=5)
81 with self.assertRaises(KeyError):
82 ObservationInfo.makeObservationInfo(unrecognized=1.5, keys="unknown")
84 with self.assertRaises(TypeError):
85 # Translator class must be a class, not instance.
86 ObservationInfo.makeObservationInfo(translator_class={})
88 with self.assertRaises(TypeError):
89 # Check that the translator class is checked.
90 ObservationInfo(boresight_airmass=1.5, translator_class=dict)
92 with self.assertRaises(TypeError):
93 ObservationInfo.makeObservationInfo(boresight_airmass=1.5, extensions=[])
95 def test_simple(self) -> None:
96 """Test that we can simplify an ObservationInfo."""
97 reference = dict(
98 boresight_airmass=1.5,
99 focus_z=1.0 * u.mm,
100 temperature=15 * u.deg_C,
101 observation_type="bias",
102 exposure_time=5 * u.ks,
103 detector_num=32,
104 datetime_begin=astropy.time.Time("2021-02-15T12:00:00", format="isot", scale="utc"),
105 )
107 obsinfo = makeObservationInfo(**reference)
108 simple = obsinfo.to_simple()
109 newinfo = ObservationInfo.from_simple(simple)
110 self.assertEqual(obsinfo, newinfo)
112 via_json = ObservationInfo.from_json(newinfo.to_json())
113 self.assertEqual(via_json, obsinfo)
115 # Sorting should be allowed (even if they are the same date).
116 _ = sorted([obsinfo, newinfo, via_json])
118 def test_pydantic(self) -> None:
119 """Test pydantic serialization."""
120 obsinfo = makeObservationInfo(boresight_airmass=1.5, tracking_radec=None)
122 jstr = obsinfo.model_dump_json()
123 from_j = ObservationInfo.model_validate_json(jstr)
124 self.assertEqual(from_j, obsinfo)
126 # Also from bytes.
127 from_j = ObservationInfo.model_validate_json(jstr.encode())
128 self.assertEqual(from_j, obsinfo)
130 # Check that you get back what you gave.
131 new_obsinfo = ObservationInfo.model_validate(from_j)
132 self.assertEqual(id(new_obsinfo), id(from_j))
134 with self.assertRaises(ValidationError):
135 ObservationInfo.model_validate([])
137 # An unregistered translator name is accepted: the name is retained
138 # but no translator instance is created.
139 unknown = ObservationInfo.model_validate({"_translator": "Unknown"})
140 self.assertEqual(unknown.translator_name, "Unknown")
141 self.assertIsNone(unknown._translator)
143 with self.assertRaises(KeyError) as cm:
144 ObservationInfo.model_validate({"random": "Unknown"})
145 self.assertIn("Unrecognized property", str(cm.exception))
147 with self.assertRaises(ValidationError) as cm:
148 ObservationInfo.model_validate({"detector_name": []})
149 self.assertIn("should be a valid string", str(cm.exception))
151 def test_embedded_pydantic(self) -> None:
152 """Test that ObservationInfo can be used inside another model."""
153 obsinfo = makeObservationInfo(boresight_airmass=math.nan, detector_name="DETECTOR")
155 test = ModelWithObsInfo(obs_info=obsinfo, number=42)
157 json_str = test.model_dump_json()
158 from_j = ModelWithObsInfo.model_validate_json(json_str)
159 self.assertEqual(from_j.number, 42)
160 self.assertEqual(from_j.obs_info.detector_name, "DETECTOR")
161 self.assertTrue(math.isnan(from_j.obs_info.boresight_airmass))
162 self.assertEqual(from_j.obs_info, obsinfo)
164 def test_altaz_extraction(self) -> None:
165 """Test that an AltAz embedded in a SkyCoord is extracted correctly."""
166 # This test came originally from ip_diffim
167 lsst_lat = -30.244639 * u.degree
168 lsst_lon = -70.749417 * u.degree
169 lsst_alt = 2663.0 * u.m
170 lsst_temperature = 20.0 * u.Celsius
171 lsst_humidity = 40.0 # in percent
172 lsst_pressure = 73892.0 * u.pascal
173 elevation = 45.0 * u.degree
174 azimuth = 110.0 * u.degree
175 rotangle = 30.0 * u.degree
177 loc = EarthLocation(lat=lsst_lat, lon=lsst_lon, height=lsst_alt)
178 airmass = 1.0 / math.sin(elevation.to_value())
180 time = Time(2026.0, format="jyear", scale="tai")
181 altaz = SkyCoord(
182 alt=elevation,
183 az=azimuth,
184 obstime=time,
185 frame="altaz",
186 location=loc,
187 )
188 obsinfo = makeObservationInfo(
189 location=loc,
190 detector_exposure_id=12345678,
191 datetime_begin=time,
192 datetime_end=time,
193 boresight_airmass=airmass,
194 boresight_rotation_angle=rotangle,
195 boresight_rotation_coord="sky",
196 temperature=lsst_temperature,
197 pressure=lsst_pressure,
198 relative_humidity=lsst_humidity,
199 tracking_radec=altaz.icrs,
200 altaz_begin=altaz,
201 observation_type="science",
202 )
203 self.assertIsInstance(obsinfo, ObservationInfo)
206if __name__ == "__main__":
207 unittest.main()