Coverage for tests / test_basics.py: 19%

104 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-17 08:50 +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. 

11 

12import math 

13import unittest 

14 

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 

20 

21import astro_metadata_translator 

22from astro_metadata_translator import ObservationInfo, makeObservationInfo 

23 

24 

25class ModelWithObsInfo(BaseModel): 

26 """Pydantic model that contains an ObservationInfo.""" 

27 

28 model_config = ConfigDict( 

29 ser_json_inf_nan="constants", # Pydantic does not preserve NaN support from child models. 

30 ) 

31 

32 obs_info: ObservationInfo 

33 number: int 

34 

35 

36class BasicTestCase(unittest.TestCase): 

37 """Test basic metadata translation functionality.""" 

38 

39 def test_basic(self) -> None: 

40 version = astro_metadata_translator.__version__ 

41 self.assertIsNotNone(version) 

42 

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(), {}) 

53 

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 ) 

62 

63 with self.assertRaises(TypeError): 

64 _ = obsinfo > obsinfo 

65 

66 with self.assertRaises(TypeError): 

67 _ = obsinfo < obsinfo 

68 

69 with self.assertRaises(TypeError): 

70 _ = obsinfo > 5 

71 

72 with self.assertRaises(TypeError): 

73 _ = obsinfo < 5 

74 

75 with self.assertRaises(AttributeError): 

76 obsinfo.boresight_airmass = 1.0 

77 

78 with self.assertRaises(ValidationError): 

79 ObservationInfo.makeObservationInfo(boresight_airmass=1.5, observation_id=5) 

80 

81 with self.assertRaises(KeyError): 

82 ObservationInfo.makeObservationInfo(unrecognized=1.5, keys="unknown") 

83 

84 with self.assertRaises(TypeError): 

85 # Translator class must be a class, not instance. 

86 ObservationInfo.makeObservationInfo(translator_class={}) 

87 

88 with self.assertRaises(TypeError): 

89 # Check that the translator class is checked. 

90 ObservationInfo(boresight_airmass=1.5, translator_class=dict) 

91 

92 with self.assertRaises(TypeError): 

93 ObservationInfo.makeObservationInfo(boresight_airmass=1.5, extensions=[]) 

94 

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 ) 

106 

107 obsinfo = makeObservationInfo(**reference) 

108 simple = obsinfo.to_simple() 

109 newinfo = ObservationInfo.from_simple(simple) 

110 self.assertEqual(obsinfo, newinfo) 

111 

112 via_json = ObservationInfo.from_json(newinfo.to_json()) 

113 self.assertEqual(via_json, obsinfo) 

114 

115 # Sorting should be allowed (even if they are the same date). 

116 _ = sorted([obsinfo, newinfo, via_json]) 

117 

118 def test_pydantic(self) -> None: 

119 """Test pydantic serialization.""" 

120 obsinfo = makeObservationInfo(boresight_airmass=1.5, tracking_radec=None) 

121 

122 jstr = obsinfo.model_dump_json() 

123 from_j = ObservationInfo.model_validate_json(jstr) 

124 self.assertEqual(from_j, obsinfo) 

125 

126 # Also from bytes. 

127 from_j = ObservationInfo.model_validate_json(jstr.encode()) 

128 self.assertEqual(from_j, obsinfo) 

129 

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)) 

133 

134 with self.assertRaises(ValidationError): 

135 ObservationInfo.model_validate([]) 

136 

137 with self.assertRaises(KeyError) as cm: 

138 ObservationInfo.model_validate({"_translator": "Unknown"}) 

139 self.assertIn("Unrecognized translator", str(cm.exception)) 

140 

141 with self.assertRaises(KeyError) as cm: 

142 ObservationInfo.model_validate({"random": "Unknown"}) 

143 self.assertIn("Unrecognized property", str(cm.exception)) 

144 

145 with self.assertRaises(ValidationError) as cm: 

146 ObservationInfo.model_validate({"detector_name": []}) 

147 self.assertIn("should be a valid string", str(cm.exception)) 

148 

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") 

152 

153 test = ModelWithObsInfo(obs_info=obsinfo, number=42) 

154 

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) 

161 

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 

174 

175 loc = EarthLocation(lat=lsst_lat, lon=lsst_lon, height=lsst_alt) 

176 airmass = 1.0 / math.sin(elevation.to_value()) 

177 

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) 

202 

203 

204if __name__ == "__main__": 

205 unittest.main()