Coverage for tests / test_makeRawVisitInfoViaObsInfo.py: 20%

115 statements  

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

21 

22import unittest 

23 

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 

34 

35import lsst.afw.image 

36from lsst.daf.base import DateTime 

37from lsst.obs.base import MakeRawVisitInfoViaObsInfo 

38 

39 

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) 

47 

48 

49class NewTranslator(FitsTranslator, StubTranslator): 

50 """Metadata translator to use for tests.""" 

51 

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 } 

69 

70 def to_location(self): 

71 return EarthLocation.from_geodetic(-70.747698, -30.244728, 2663.0) 

72 

73 def to_detector_exposure_id(self): 

74 return self.to_exposure_id() 

75 

76 def to_tracking_radec(self) -> astropy.coordinates.SkyCoord | None: 

77 return tracking_from_degree_headers(self, ["RADESYS"], (("RA_DEG", "DEC_DEG"),)) 

78 

79 def to_altaz_begin(self) -> astropy.coordinates.AltAz | None: 

80 return altaz_from_degree_headers(self, (("TELALT", "TELAZ"),), self.to_datetime_begin()) 

81 

82 

83class MakeTestableVisitInfo(MakeRawVisitInfoViaObsInfo): 

84 """Test class for VisitInfo construction.""" 

85 

86 metadataTranslator = NewTranslator 

87 

88 

89class TestMakeRawVisitInfoViaObsInfo(unittest.TestCase): 

90 """Test VisitInfo construction.""" 

91 

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 

104 

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 } 

134 

135 def testMakeRawVisitInfoViaObsInfo(self): 

136 maker = MakeTestableVisitInfo() 

137 beforeLength = len(self.header) 

138 

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) 

143 

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) 

171 

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) 

186 

187 def testObservationInfo2VisitInfo(self): 

188 with self.assertWarns(UserWarning): 

189 obsInfo = ObservationInfo(self.header, translator_class=NewTranslator) 

190 

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) 

196 

197 # Convert it back to an ObservationInfo. 

198 obsInfo2 = MakeRawVisitInfoViaObsInfo.visitInfo2observationInfo(visitInfo) 

199 

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 ) 

227 

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

252 

253 

254if __name__ == "__main__": 254 ↛ 255line 254 didn't jump to line 255 because the condition on line 254 was never true

255 unittest.main()