Coverage for tests / test_makeRawVisitInfoViaObsInfo.py: 20%
115 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-18 08:46 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-18 08:46 +0000
1# This file is part of obs_base.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
22import unittest
24import astropy.coordinates
25import astropy.units as u
26import numpy as np
27from astro_metadata_translator import FitsTranslator, ObservationInfo, StubTranslator
28from astro_metadata_translator.translators.helpers import (
29 altaz_from_degree_headers,
30 tracking_from_degree_headers,
31)
32from astropy.coordinates import EarthLocation
33from astropy.time import Time
35import lsst.afw.image
36from lsst.daf.base import DateTime
37from lsst.obs.base import MakeRawVisitInfoViaObsInfo
40def _q_to_float(q: u.Quantity, unit: u.UnitBase | str) -> float:
41 values = q.to_value(unit=unit)
42 if isinstance(values, np.ndarray):
43 raise ValueError(
44 f"Converting quantity to a float failed because unexpectedly got more than one float: {values}"
45 )
46 return float(values)
49class NewTranslator(FitsTranslator, StubTranslator):
50 """Metadata translator to use for tests."""
52 _trivial_map = {
53 "exposure_time": ("EXPTIME", {"unit": u.s}),
54 "dark_time": ("DARKTM", {"unit": u.s}),
55 "exposure_id": "EXP-ID",
56 "boresight_airmass": "AMASS",
57 "boresight_rotation_angle": ("SKYROT", {"unit": u.deg}),
58 "boresight_rotation_coord": "SKYC",
59 "observation_type": "OBSTYPE",
60 "science_program": "PROGRAM",
61 "observation_reason": "REASON",
62 "object": "OBJECT",
63 "has_simulated_content": "SIMULATE",
64 "relative_humidity": "RELHUM",
65 "temperature": ("AIR_TEMP", {"unit": u.deg_C}),
66 "pressure": ("PRESSURE", {"unit": u.kPa}),
67 "focus_z": ("FOCUSZ", {"unit": u.mm}),
68 }
70 def to_location(self):
71 return EarthLocation.from_geodetic(-70.747698, -30.244728, 2663.0)
73 def to_detector_exposure_id(self):
74 return self.to_exposure_id()
76 def to_tracking_radec(self) -> astropy.coordinates.SkyCoord | None:
77 return tracking_from_degree_headers(self, ["RADESYS"], (("RA_DEG", "DEC_DEG"),))
79 def to_altaz_begin(self) -> astropy.coordinates.AltAz | None:
80 return altaz_from_degree_headers(self, (("TELALT", "TELAZ"),), self.to_datetime_begin())
83class MakeTestableVisitInfo(MakeRawVisitInfoViaObsInfo):
84 """Test class for VisitInfo construction."""
86 metadataTranslator = NewTranslator
89class TestMakeRawVisitInfoViaObsInfo(unittest.TestCase):
90 """Test VisitInfo construction."""
92 def setUp(self):
93 # Reference values
94 self.exposure_time = 6.2 * u.s
95 self.dark_time = 7.0 * u.s
96 self.boresight_airmass = 1.5
97 self.exposure_id = 54321
98 self.datetime_begin = Time("2001-01-02T03:04:05.123456789", format="isot", scale="utc")
99 self.datetime_begin.precision = 9
100 self.datetime_end = self.datetime_begin + self.exposure_time
101 self.datetime_end.precision = 9
102 self.focus_z = 1.5 * u.mm
103 self.pressure = 101.0 * u.kPa
105 self.header = {
106 "DATE-OBS": self.datetime_begin.isot,
107 "DATE-END": self.datetime_end.isot,
108 "INSTRUME": "SomeCamera",
109 "TELESCOP": "LSST",
110 "TIMESYS": "UTC",
111 "SKYROT": 45.0,
112 "SKYC": "sky",
113 "EXPTIME": _q_to_float(self.exposure_time, u.s),
114 "DARKTM": _q_to_float(self.dark_time, u.s),
115 "EXP-ID": self.exposure_id,
116 "FOCUSZ": _q_to_float(self.focus_z, u.mm),
117 "EXTRA1": "an abitrary key and value",
118 "EXTRA2": 5,
119 "OBSTYPE": "test type",
120 "PROGRAM": "test program",
121 "REASON": "test reason",
122 "OBJECT": "test object",
123 "SIMULATE": True,
124 "RELHUM": 42.0,
125 "AIR_TEMP": 1.0,
126 "PRESSURE": _q_to_float(self.pressure, u.kPa),
127 "AMASS": self.boresight_airmass,
128 "RADESYS": "FK5",
129 "RA_DEG": 45.0,
130 "DEC_DEG": -30.0,
131 "TELALT": 60.0,
132 "TELAZ": 180.0,
133 }
135 def testMakeRawVisitInfoViaObsInfo(self):
136 maker = MakeTestableVisitInfo()
137 beforeLength = len(self.header)
139 # Capture the warnings from StubTranslator since they are
140 # confusing to people but irrelevant for the test.
141 with self.assertWarns(UserWarning):
142 visitInfo = maker(self.header)
144 self.assertAlmostEqual(visitInfo.getExposureTime(), _q_to_float(self.exposure_time, u.s))
145 self.assertAlmostEqual(visitInfo.getDarkTime(), _q_to_float(self.dark_time, u.s))
146 self.assertEqual(visitInfo.id, self.exposure_id)
147 self.assertEqual(visitInfo.getDate(), DateTime("2001-01-02T03:04:08.223456789Z", DateTime.UTC))
148 # The header can possibly grow with header fix up provenance.
149 self.assertGreaterEqual(len(self.header), beforeLength)
150 self.assertEqual(visitInfo.getInstrumentLabel(), "SomeCamera")
151 self.assertAlmostEqual(visitInfo.getBoresightRaDec().getRa().asDegrees(), 45.0, places=5)
152 self.assertAlmostEqual(visitInfo.getBoresightRaDec().getDec().asDegrees(), -30.0, places=5)
153 self.assertAlmostEqual(visitInfo.getBoresightAzAlt().getLongitude().asDegrees(), 180.0, places=7)
154 self.assertAlmostEqual(visitInfo.getBoresightAzAlt().getLatitude().asDegrees(), 60.0, places=7)
155 self.assertEqual(visitInfo.getBoresightAirmass(), self.boresight_airmass)
156 self.assertAlmostEqual(visitInfo.getBoresightRotAngle().asDegrees(), 45.0)
157 self.assertEqual(visitInfo.getRotType(), lsst.afw.image.RotType.SKY)
158 self.assertAlmostEqual(visitInfo.getObservatory().getLongitude().asDegrees(), -70.747698)
159 self.assertAlmostEqual(visitInfo.getObservatory().getLatitude().asDegrees(), -30.244728)
160 self.assertAlmostEqual(visitInfo.getObservatory().getElevation(), 2663.0)
161 self.assertEqual(visitInfo.getWeather().getHumidity(), 42.0)
162 self.assertEqual(visitInfo.getWeather().getAirTemperature(), 1.0)
163 self.assertEqual(visitInfo.getWeather().getAirPressure(), _q_to_float(self.pressure, u.Pa))
164 # Check focusZ with default value from astro_metadata_translator
165 self.assertEqual(visitInfo.getFocusZ(), _q_to_float(self.focus_z, u.mm))
166 self.assertEqual(visitInfo.observationType, "test type")
167 self.assertEqual(visitInfo.scienceProgram, "test program")
168 self.assertEqual(visitInfo.observationReason, "test reason")
169 self.assertEqual(visitInfo.object, "test object")
170 self.assertEqual(visitInfo.hasSimulatedContent, True)
172 def testMakeRawVisitInfoViaObsInfo_empty(self):
173 """Test that empty metadata fields are set to appropriate defaults."""
174 maker = MakeTestableVisitInfo()
175 # Capture the warnings from StubTranslator since they are
176 # confusing to people but irrelevant for the test.
177 with self.assertWarns(UserWarning):
178 with self.assertLogs(level="WARNING"):
179 visitInfo = maker({})
180 self.assertTrue(np.isnan(visitInfo.focusZ))
181 self.assertEqual(visitInfo.observationType, "")
182 self.assertEqual(visitInfo.scienceProgram, "")
183 self.assertEqual(visitInfo.observationReason, "")
184 self.assertEqual(visitInfo.object, "")
185 self.assertEqual(visitInfo.hasSimulatedContent, False)
187 def testObservationInfo2VisitInfo(self):
188 with self.assertWarns(UserWarning):
189 obsInfo = ObservationInfo(self.header, translator_class=NewTranslator)
191 # Check that a couple of values look correct.
192 visitInfo = MakeRawVisitInfoViaObsInfo.observationInfo2visitInfo(obsInfo)
193 self.assertIsInstance(visitInfo, lsst.afw.image.VisitInfo)
194 self.assertAlmostEqual(visitInfo.getExposureTime(), _q_to_float(self.exposure_time, u.s))
195 self.assertEqual(visitInfo.id, self.exposure_id)
197 # Convert it back to an ObservationInfo.
198 obsInfo2 = MakeRawVisitInfoViaObsInfo.visitInfo2observationInfo(visitInfo)
200 # We can not compare all fields because some fields are lost in
201 # conversion to VisitInfo.
202 to_compare = (
203 "datetime_begin",
204 "altaz_begin",
205 "boresight_airmass",
206 "boresight_rotation_angle",
207 "boresight_rotation_coord",
208 "dark_time",
209 "datetime_end",
210 "exposure_group",
211 "exposure_id",
212 "exposure_time",
213 "exposure_time_requested",
214 "focus_z",
215 "has_simulated_content",
216 "location",
217 "object",
218 "observation_reason",
219 "observation_type",
220 "observing_day",
221 "pressure",
222 "relative_humidity",
223 "science_program",
224 "temperature",
225 "tracking_radec",
226 )
228 for prop in to_compare:
229 previous = getattr(obsInfo, prop)
230 new = getattr(obsInfo2, prop)
231 with self.subTest(prop=prop):
232 match new:
233 case astropy.coordinates.AltAz():
234 self.assertAlmostEqual(new.az, previous.az)
235 self.assertAlmostEqual(new.alt, previous.alt)
236 case astropy.coordinates.SkyCoord():
237 self.assertAlmostEqual(new.icrs.ra, previous.icrs.ra)
238 self.assertAlmostEqual(new.icrs.dec, previous.icrs.dec)
239 case astropy.coordinates.EarthLocation():
240 for new_pos, previous_pos in zip(
241 new.to_geocentric(), previous.to_geocentric(), strict=True
242 ):
243 self.assertAlmostEqual(new_pos, previous_pos)
244 case astropy.time.Time():
245 self.assertEqual(new, previous)
246 case float() | u.Quantity():
247 self.assertEqual(new, previous, f"Comparing float-like property {prop}")
248 case int() | str():
249 self.assertEqual(new, previous, f"Comparing int or string property {prop}")
250 case _:
251 raise RuntimeError(f"Encountered unexpected type for property {prop}")
254if __name__ == "__main__": 254 ↛ 255line 254 didn't jump to line 255 because the condition on line 254 was never true
255 unittest.main()