Coverage for tests / test_translation.py: 14%

137 statements  

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

13import os.path 

14import unittest 

15 

16from astropy.time import Time, TimeDelta 

17 

18from astro_metadata_translator import FitsTranslator, ObservationInfo, StubTranslator 

19 

20TESTDIR = os.path.abspath(os.path.dirname(__file__)) 

21 

22 

23class InstrumentTestTranslator(FitsTranslator, StubTranslator): 

24 """Simple FITS-like translator to test the infrastructure.""" 

25 

26 # Needs a name to be registered 

27 name = "TestTranslator" 

28 

29 # Indicate the instrument this class understands 

30 supported_instrument = "SCUBA_test" 

31 

32 # Some new mappings, including an override 

33 _trivial_map = { 

34 "telescope": "TELCODE", 

35 "exposure_id": "EXPID", 

36 "relative_humidity": "HUMIDITY", 

37 "detector_name": "DETNAME", 

38 "observation_id": "OBSID", 

39 } 

40 

41 # Add translator method to test joining 

42 def to_physical_filter(self) -> str: 

43 return self._join_keyword_values(["DETNAME", "HUMIDITY"], delim="_") 

44 

45 

46class MissingMethodsTranslator(FitsTranslator): 

47 """Translator class that does not implement all the methods.""" 

48 

49 pass 

50 

51 

52class TranslatorTestCase(unittest.TestCase): 

53 """Test core translation infrastructure.""" 

54 

55 def setUp(self) -> None: 

56 # Known simple header 

57 self.header = { 

58 "TELESCOP": "JCMT", 

59 "TELCODE": "LSST", 

60 "INSTRUME": "SCUBA_test", 

61 "DATE-OBS": "2000-01-01T01:00:01.500", 

62 "DATE-END": "2000-01-01T02:00:01.500", 

63 "OBSGEO-X": "-5464588.84421314", 

64 "OBSGEO-Y": "-2493000.19137644", 

65 "OBSGEO-Z": "2150653.35350771", 

66 "OBSID": "20000101_00002", 

67 "EXPID": "22", # Should cast to a number 

68 "DETNAME": 76, # Should cast to a string 

69 "HUMIDITY": "55", # Should cast to a float 

70 "BAZ": "bar", 

71 } 

72 

73 def test_manual_translation(self) -> None: 

74 header = self.header 

75 translator = FitsTranslator(header) 

76 

77 # Treat the header as standard FITS 

78 self.assertFalse(FitsTranslator.can_translate(header)) 

79 self.assertEqual(translator.to_telescope(), "JCMT") 

80 self.assertEqual(translator.to_instrument(), "SCUBA_test") 

81 self.assertEqual(translator.to_datetime_begin(), Time(header["DATE-OBS"], format="isot")) 

82 

83 # This class will issue warnings 

84 with self.assertLogs("astro_metadata_translator") as cm: 

85 

86 class InstrumentTestTranslatorExtras(InstrumentTestTranslator): 

87 """Version of InstrumentTestTranslator with unexpected 

88 fields. 

89 """ 

90 

91 name = "InstrumentTestTranslatorExtras" 

92 _trivial_map = {"foobar": "BAZ"} 

93 _const_map = {"format": "HDF5"} 

94 

95 self.assertIn("Unexpected trivial", cm.output[0]) 

96 self.assertIn("Unexpected constant", cm.output[1]) 

97 

98 # Use the special test translator instead 

99 translator = InstrumentTestTranslatorExtras(header) 

100 self.assertTrue(InstrumentTestTranslator.can_translate(header)) 

101 self.assertEqual(translator.to_telescope(), "LSST") 

102 self.assertEqual(translator.to_instrument(), "SCUBA_test") 

103 self.assertEqual(translator.to_format(), "HDF5") 

104 self.assertEqual(translator.to_foobar(), "bar") 

105 

106 def test_translator(self) -> None: 

107 header = self.header 

108 

109 # Specify a translation class 

110 with self.assertWarns(UserWarning): 

111 # Since the translator is incomplete it should issue warnings 

112 v1 = ObservationInfo(header, translator_class=InstrumentTestTranslator) 

113 self.assertEqual(v1.instrument, "SCUBA_test") 

114 self.assertEqual(v1.telescope, "LSST") 

115 self.assertEqual(v1.exposure_id, 22) 

116 self.assertIsInstance(v1.exposure_id, int) 

117 self.assertEqual(v1.detector_name, "76") 

118 self.assertEqual(v1.relative_humidity, 55.0) 

119 self.assertIsInstance(v1.relative_humidity, float) 

120 self.assertEqual(v1.physical_filter, "76_55") 

121 

122 # Now automated class 

123 with self.assertWarns(UserWarning): 

124 # Since the translator is incomplete it should issue warnings 

125 v1 = ObservationInfo(header) 

126 self.assertEqual(v1.instrument, "SCUBA_test") 

127 self.assertEqual(v1.telescope, "LSST") 

128 

129 location = v1.location.to_geodetic() 

130 self.assertAlmostEqual(float(location.height.to("m").to_value()), 4123.0, places=1) 

131 

132 # Check that headers have been removed 

133 new_hdr = v1.stripped_header() 

134 self.assertNotIn("INSTRUME", new_hdr) 

135 self.assertNotIn("OBSGEO-X", new_hdr) 

136 self.assertIn("TELESCOP", new_hdr) 

137 

138 # Check the list of cards that were used 

139 used = v1.cards_used 

140 self.assertIn("INSTRUME", used) 

141 self.assertIn("OBSGEO-Y", used) 

142 self.assertNotIn("TELESCOP", used) 

143 

144 # Stringification 

145 summary = str(v1) 

146 self.assertIn("datetime_begin", summary) 

147 

148 # Create with a subset of properties 

149 v2 = ObservationInfo( 

150 header, 

151 translator_class=InstrumentTestTranslator, 

152 subset={"telescope", "datetime_begin", "exposure_group"}, 

153 ) 

154 

155 self.assertEqual(v2.telescope, v1.telescope) 

156 self.assertEqual(v2.datetime_begin, v2.datetime_begin) 

157 self.assertIsNone(v2.datetime_end) 

158 self.assertIsNone(v2.location) 

159 self.assertIsNone(v2.observation_id) 

160 

161 # Use the new, explicit constructor. 

162 v3 = ObservationInfo.from_header( 

163 header, 

164 translator_class=InstrumentTestTranslator, 

165 subset={"telescope", "datetime_begin", "exposure_group"}, 

166 ) 

167 self.assertEqual(v3, v2) 

168 

169 with self.assertRaises(TypeError): 

170 # This fails a not-a-class check. 

171 ObservationInfo(header, translator_class={}) 

172 with self.assertRaises(TypeError): 

173 # Check that the translator class is checked. 

174 ObservationInfo(header, translator_class=dict) 

175 with self.assertRaises(ValueError): 

176 ObservationInfo(header, keyword="unknown") 

177 with self.assertRaises(ValueError): 

178 ObservationInfo.from_header(header, subset=set()) 

179 

180 def test_corrections(self) -> None: 

181 """Apply corrections before translation.""" 

182 header = self.header 

183 

184 # Specify a translation class 

185 with self.assertWarns(UserWarning): 

186 # Since the translator is incomplete it should issue warnings 

187 v1 = ObservationInfo( 

188 header, 

189 translator_class=InstrumentTestTranslator, 

190 search_path=[os.path.join(TESTDIR, "data", "corrections")], 

191 ) 

192 

193 # These values should match the expected translation 

194 self.assertEqual(v1.instrument, "SCUBA_test") 

195 self.assertEqual(v1.detector_name, "76") 

196 self.assertEqual(v1.relative_humidity, 55.0) 

197 self.assertIsInstance(v1.relative_humidity, float) 

198 self.assertEqual(v1.physical_filter, "76_55") 

199 

200 # These two should be the "corrected" values 

201 self.assertEqual(v1.telescope, "AuxTel") 

202 self.assertEqual(v1.exposure_id, 42) 

203 

204 def test_failures(self) -> None: 

205 header = {} 

206 

207 with self.assertRaises(TypeError): 

208 ObservationInfo(header, translator_class=ObservationInfo) 

209 

210 with self.assertRaises(ValueError): 

211 ObservationInfo( 

212 header, translator_class=InstrumentTestTranslator, subset={"definitely_not_known"} 

213 ) 

214 

215 with self.assertRaises(ValueError): 

216 ObservationInfo( 

217 header, translator_class=InstrumentTestTranslator, required={"definitely_not_known"} 

218 ) 

219 

220 with self.assertLogs("astro_metadata_translator"): 

221 with self.assertWarns(UserWarning): 

222 ObservationInfo(header, translator_class=InstrumentTestTranslator, pedantic=False) 

223 

224 with self.assertLogs("astro_metadata_translator", level=logging.DEBUG) as cm: 

225 with self.assertWarns(UserWarning): 

226 ObservationInfo(header, translator_class=InstrumentTestTranslator, pedantic=False, quiet=True) 

227 # Check that at least one of those DEBUG logs was a translation 

228 # warning. 

229 self.assertIn("DEBUG:astro_metadata_translator.observationInfo:Calculation of property", cm.output[0]) 

230 self.assertIn( 

231 "DEBUG:astro_metadata_translator.observationInfo:Ignoring Error calculating property", 

232 cm.output[1], 

233 ) 

234 

235 with self.assertRaises(KeyError): 

236 with self.assertWarns(UserWarning): 

237 ObservationInfo(header, translator_class=InstrumentTestTranslator, pedantic=True) 

238 

239 # Pass in header where the key does exist but the value is bad. 

240 bad_header = { 

241 "OBSGEO-X": "not-float", 

242 "OBSGEO-Y": "not-float", 

243 "OBSGEO-Z": "not-float", 

244 "TELESCOP": "JCMT", 

245 "TELCODE": "LSST", 

246 "INSTRUME": "SCUBA_test", 

247 } 

248 

249 with self.assertLogs("astro_metadata_translator"): 

250 with self.assertWarns(UserWarning): 

251 ObservationInfo(bad_header, translator_class=InstrumentTestTranslator, pedantic=False) 

252 

253 with self.assertLogs("astro_metadata_translator"): 

254 with self.assertWarns(UserWarning): 

255 ObservationInfo( 

256 header, translator_class=InstrumentTestTranslator, pedantic=False, filename="testfile1" 

257 ) 

258 

259 with self.assertRaises(KeyError): 

260 with self.assertWarns(UserWarning): 

261 ObservationInfo( 

262 header, translator_class=InstrumentTestTranslator, pedantic=True, filename="testfile2" 

263 ) 

264 

265 with self.assertRaises(NotImplementedError): 

266 with self.assertLogs("astro_metadata_translator", level="WARN"): 

267 ObservationInfo(header, translator_class=MissingMethodsTranslator) 

268 

269 with self.assertRaises(KeyError): 

270 with self.assertWarns(UserWarning): 

271 with self.assertLogs("astro_metadata_translator", level="WARN"): 

272 ObservationInfo( 

273 header, 

274 translator_class=InstrumentTestTranslator, 

275 pedantic=False, 

276 required={"boresight_airmass"}, 

277 ) 

278 

279 def test_observing_date(self) -> None: 

280 """Test the observing_date class methods.""" 

281 date1 = Time("2023-07-06T13:00", format="isot", scale="tai") 

282 day_obs = StubTranslator.observing_date_to_observing_day(date1, 12 * 3600) 

283 self.assertEqual(day_obs, 20230706) 

284 

285 date2 = Time("2023-07-07T11:00", format="isot", scale="tai") 

286 day_obs = StubTranslator.observing_date_to_observing_day(date2, TimeDelta("0.5d", scale="tai")) 

287 self.assertEqual(day_obs, 20230706) 

288 day_obs = StubTranslator.observing_date_to_observing_day(date2, 1.0) 

289 self.assertEqual(day_obs, 20230707) 

290 

291 

292if __name__ == "__main__": 

293 unittest.main()