Coverage for tests / test_translation.py: 14%

131 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-02-19 01:05 -0800

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 os.path 

13import unittest 

14 

15from astropy.time import Time, TimeDelta 

16 

17from astro_metadata_translator import FitsTranslator, ObservationInfo, StubTranslator 

18 

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

20 

21 

22class InstrumentTestTranslator(FitsTranslator, StubTranslator): 

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

24 

25 # Needs a name to be registered 

26 name = "TestTranslator" 

27 

28 # Indicate the instrument this class understands 

29 supported_instrument = "SCUBA_test" 

30 

31 # Some new mappings, including an override 

32 _trivial_map = { 

33 "telescope": "TELCODE", 

34 "exposure_id": "EXPID", 

35 "relative_humidity": "HUMIDITY", 

36 "detector_name": "DETNAME", 

37 "observation_id": "OBSID", 

38 } 

39 

40 # Add translator method to test joining 

41 def to_physical_filter(self) -> str: 

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

43 

44 

45class MissingMethodsTranslator(FitsTranslator): 

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

47 

48 pass 

49 

50 

51class TranslatorTestCase(unittest.TestCase): 

52 """Test core translation infrastructure.""" 

53 

54 def setUp(self) -> None: 

55 # Known simple header 

56 self.header = { 

57 "TELESCOP": "JCMT", 

58 "TELCODE": "LSST", 

59 "INSTRUME": "SCUBA_test", 

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

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

62 "OBSGEO-X": "-5464588.84421314", 

63 "OBSGEO-Y": "-2493000.19137644", 

64 "OBSGEO-Z": "2150653.35350771", 

65 "OBSID": "20000101_00002", 

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

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

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

69 "BAZ": "bar", 

70 } 

71 

72 def test_manual_translation(self) -> None: 

73 header = self.header 

74 translator = FitsTranslator(header) 

75 

76 # Treat the header as standard FITS 

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

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

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

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

81 

82 # This class will issue warnings 

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

84 

85 class InstrumentTestTranslatorExtras(InstrumentTestTranslator): 

86 """Version of InstrumentTestTranslator with unexpected 

87 fields. 

88 """ 

89 

90 name = "InstrumentTestTranslatorExtras" 

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

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

93 

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

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

96 

97 # Use the special test translator instead 

98 translator = InstrumentTestTranslatorExtras(header) 

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

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

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

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

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

104 

105 def test_translator(self) -> None: 

106 header = self.header 

107 

108 # Specify a translation class 

109 with self.assertWarns(UserWarning): 

110 # Since the translator is incomplete it should issue warnings 

111 v1 = ObservationInfo(header, translator_class=InstrumentTestTranslator) 

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

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

114 self.assertEqual(v1.exposure_id, 22) 

115 self.assertIsInstance(v1.exposure_id, int) 

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

117 self.assertEqual(v1.relative_humidity, 55.0) 

118 self.assertIsInstance(v1.relative_humidity, float) 

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

120 

121 # Now automated class 

122 with self.assertWarns(UserWarning): 

123 # Since the translator is incomplete it should issue warnings 

124 v1 = ObservationInfo(header) 

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

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

127 

128 location = v1.location.to_geodetic() 

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

130 

131 # Check that headers have been removed 

132 new_hdr = v1.stripped_header() 

133 self.assertNotIn("INSTRUME", new_hdr) 

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

135 self.assertIn("TELESCOP", new_hdr) 

136 

137 # Check the list of cards that were used 

138 used = v1.cards_used 

139 self.assertIn("INSTRUME", used) 

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

141 self.assertNotIn("TELESCOP", used) 

142 

143 # Stringification 

144 summary = str(v1) 

145 self.assertIn("datetime_begin", summary) 

146 

147 # Create with a subset of properties 

148 v2 = ObservationInfo( 

149 header, 

150 translator_class=InstrumentTestTranslator, 

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

152 ) 

153 

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

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

156 self.assertIsNone(v2.datetime_end) 

157 self.assertIsNone(v2.location) 

158 self.assertIsNone(v2.observation_id) 

159 

160 # Use the new, explicit constructor. 

161 v3 = ObservationInfo.from_header( 

162 header, 

163 translator_class=InstrumentTestTranslator, 

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

165 ) 

166 self.assertEqual(v3, v2) 

167 

168 with self.assertRaises(TypeError): 

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

170 ObservationInfo(header, translator_class={}) 

171 with self.assertRaises(TypeError): 

172 # Check that the translator class is checked. 

173 ObservationInfo(header, translator_class=dict) 

174 with self.assertRaises(ValueError): 

175 ObservationInfo(header, keyword="unknown") 

176 with self.assertRaises(ValueError): 

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

178 

179 def test_corrections(self) -> None: 

180 """Apply corrections before translation.""" 

181 header = self.header 

182 

183 # Specify a translation class 

184 with self.assertWarns(UserWarning): 

185 # Since the translator is incomplete it should issue warnings 

186 v1 = ObservationInfo( 

187 header, 

188 translator_class=InstrumentTestTranslator, 

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

190 ) 

191 

192 # These values should match the expected translation 

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

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

195 self.assertEqual(v1.relative_humidity, 55.0) 

196 self.assertIsInstance(v1.relative_humidity, float) 

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

198 

199 # These two should be the "corrected" values 

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

201 self.assertEqual(v1.exposure_id, 42) 

202 

203 def test_failures(self) -> None: 

204 header = {} 

205 

206 with self.assertRaises(TypeError): 

207 ObservationInfo(header, translator_class=ObservationInfo) 

208 

209 with self.assertRaises(ValueError): 

210 ObservationInfo( 

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

212 ) 

213 

214 with self.assertRaises(ValueError): 

215 ObservationInfo( 

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

217 ) 

218 

219 with self.assertLogs("astro_metadata_translator"): 

220 with self.assertWarns(UserWarning): 

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

222 

223 with self.assertRaises(KeyError): 

224 with self.assertWarns(UserWarning): 

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

226 

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

228 bad_header = { 

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

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

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

232 "TELESCOP": "JCMT", 

233 "TELCODE": "LSST", 

234 "INSTRUME": "SCUBA_test", 

235 } 

236 

237 with self.assertLogs("astro_metadata_translator"): 

238 with self.assertWarns(UserWarning): 

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

240 

241 with self.assertLogs("astro_metadata_translator"): 

242 with self.assertWarns(UserWarning): 

243 ObservationInfo( 

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

245 ) 

246 

247 with self.assertRaises(KeyError): 

248 with self.assertWarns(UserWarning): 

249 ObservationInfo( 

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

251 ) 

252 

253 with self.assertRaises(NotImplementedError): 

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

255 ObservationInfo(header, translator_class=MissingMethodsTranslator) 

256 

257 with self.assertRaises(KeyError): 

258 with self.assertWarns(UserWarning): 

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

260 ObservationInfo( 

261 header, 

262 translator_class=InstrumentTestTranslator, 

263 pedantic=False, 

264 required={"boresight_airmass"}, 

265 ) 

266 

267 def test_observing_date(self) -> None: 

268 """Test the observing_date class methods.""" 

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

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

271 self.assertEqual(day_obs, 20230706) 

272 

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

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

275 self.assertEqual(day_obs, 20230706) 

276 day_obs = StubTranslator.observing_date_to_observing_day(date2, 1.0) 

277 self.assertEqual(day_obs, 20230707) 

278 

279 

280if __name__ == "__main__": 

281 unittest.main()