Coverage for tests / test_basics.py: 19%
104 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-30 08:43 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-30 08:43 +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 with self.assertRaises(KeyError) as cm:
138 ObservationInfo.model_validate({"_translator": "Unknown"})
139 self.assertIn("Unrecognized translator", str(cm.exception))
141 with self.assertRaises(KeyError) as cm:
142 ObservationInfo.model_validate({"random": "Unknown"})
143 self.assertIn("Unrecognized property", str(cm.exception))
145 with self.assertRaises(ValidationError) as cm:
146 ObservationInfo.model_validate({"detector_name": []})
147 self.assertIn("should be a valid string", str(cm.exception))
149 def test_embedded_pydantic(self) -> None:
150 """Test that ObservationInfo can be used inside another model."""
151 obsinfo = makeObservationInfo(boresight_airmass=math.nan, detector_name="DETECTOR")
153 test = ModelWithObsInfo(obs_info=obsinfo, number=42)
155 json_str = test.model_dump_json()
156 from_j = ModelWithObsInfo.model_validate_json(json_str)
157 self.assertEqual(from_j.number, 42)
158 self.assertEqual(from_j.obs_info.detector_name, "DETECTOR")
159 self.assertTrue(math.isnan(from_j.obs_info.boresight_airmass))
160 self.assertEqual(from_j.obs_info, obsinfo)
162 def test_altaz_extraction(self) -> None:
163 """Test that an AltAz embedded in a SkyCoord is extracted correctly."""
164 # This test came originally from ip_diffim
165 lsst_lat = -30.244639 * u.degree
166 lsst_lon = -70.749417 * u.degree
167 lsst_alt = 2663.0 * u.m
168 lsst_temperature = 20.0 * u.Celsius
169 lsst_humidity = 40.0 # in percent
170 lsst_pressure = 73892.0 * u.pascal
171 elevation = 45.0 * u.degree
172 azimuth = 110.0 * u.degree
173 rotangle = 30.0 * u.degree
175 loc = EarthLocation(lat=lsst_lat, lon=lsst_lon, height=lsst_alt)
176 airmass = 1.0 / math.sin(elevation.to_value())
178 time = Time(2026.0, format="jyear", scale="tai")
179 altaz = SkyCoord(
180 alt=elevation,
181 az=azimuth,
182 obstime=time,
183 frame="altaz",
184 location=loc,
185 )
186 obsinfo = makeObservationInfo(
187 location=loc,
188 detector_exposure_id=12345678,
189 datetime_begin=time,
190 datetime_end=time,
191 boresight_airmass=airmass,
192 boresight_rotation_angle=rotangle,
193 boresight_rotation_coord="sky",
194 temperature=lsst_temperature,
195 pressure=lsst_pressure,
196 relative_humidity=lsst_humidity,
197 tracking_radec=altaz.icrs,
198 altaz_begin=altaz,
199 observation_type="science",
200 )
201 self.assertIsInstance(obsinfo, ObservationInfo)
204if __name__ == "__main__":
205 unittest.main()