Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 

12"""Represent standard metadata from instrument headers""" 

13 

14__all__ = ("ObservationInfo", "makeObservationInfo") 

15 

16import itertools 

17import logging 

18import copy 

19 

20import astropy.time 

21from astropy.coordinates import SkyCoord, AltAz 

22 

23from .translator import MetadataTranslator 

24from .properties import PROPERTIES 

25from .headers import fix_header 

26 

27log = logging.getLogger(__name__) 

28 

29 

30class ObservationInfo: 

31 """Standardized representation of an instrument header for a single 

32 exposure observation. 

33 

34 Parameters 

35 ---------- 

36 header : `dict`-like 

37 Representation of an instrument header accessible as a `dict`. 

38 May be updated with header corrections if corrections are found. 

39 filename : `str`, optional 

40 Name of the file whose header is being translated. For some 

41 datasets with missing header information this can sometimes 

42 allow for some fixups in translations. 

43 translator_class : `MetadataTranslator`-class, optional 

44 If not `None`, the class to use to translate the supplied headers 

45 into standard form. Otherwise each registered translator class will 

46 be asked in turn if it knows how to translate the supplied header. 

47 pedantic : `bool`, optional 

48 If True the translation must succeed for all properties. If False 

49 individual property translations must all be implemented but can fail 

50 and a warning will be issued. 

51 search_path : iterable, optional 

52 Override search paths to use during header fix up. 

53 

54 Raises 

55 ------ 

56 ValueError 

57 Raised if the supplied header was not recognized by any of the 

58 registered translators. 

59 TypeError 

60 Raised if the supplied translator class was not a MetadataTranslator. 

61 KeyError 

62 Raised if a translation fails and pedantic mode is enabled. 

63 NotImplementedError 

64 Raised if the selected translator does not support a required 

65 property. 

66 

67 Notes 

68 ----- 

69 Headers will be corrected if correction files are located and this will 

70 modify the header provided to the constructor. 

71 """ 

72 

73 _PROPERTIES = PROPERTIES 

74 """All the properties supported by this class with associated 

75 documentation.""" 

76 

77 def __init__(self, header, filename=None, translator_class=None, pedantic=False, 

78 search_path=None): 

79 

80 # Initialize the empty object 

81 self._header = {} 

82 self.filename = filename 

83 self._translator = None 

84 self.translator_class_name = "<None>" 

85 

86 # To allow makeObservationInfo to work, we special case a None 

87 # header 

88 if header is None: 

89 return 

90 

91 # Fix up the header (if required) 

92 fix_header(header, translator_class=translator_class, filename=filename, 

93 search_path=search_path) 

94 

95 # Store the supplied header for later stripping 

96 self._header = header 

97 

98 if translator_class is None: 

99 translator_class = MetadataTranslator.determine_translator(header, filename=filename) 

100 elif not issubclass(translator_class, MetadataTranslator): 

101 raise TypeError(f"Translator class must be a MetadataTranslator, not {translator_class}") 

102 

103 # Create an instance for this header 

104 translator = translator_class(header, filename=filename) 

105 

106 # Store the translator 

107 self._translator = translator 

108 self.translator_class_name = translator_class.__name__ 

109 

110 # Form file information string in case we need an error message 

111 if filename: 

112 file_info = f" and file {filename}" 

113 else: 

114 file_info = "" 

115 

116 # Loop over each property and request the translated form 

117 for t in self._PROPERTIES: 

118 # prototype code 

119 method = f"to_{t}" 

120 property = f"_{t}" 

121 

122 try: 

123 value = getattr(translator, method)() 

124 except NotImplementedError as e: 

125 raise NotImplementedError(f"No translation exists for property '{t}'" 

126 f" using translator {translator.__class__}") from e 

127 except KeyError as e: 

128 err_msg = f"Error calculating property '{t}' using translator {translator.__class__}" \ 

129 f"{file_info}" 

130 if pedantic: 

131 raise KeyError(err_msg) from e 

132 else: 

133 log.debug("Calculation of property '%s' failed with header: %s", t, header) 

134 log.warning(f"Ignoring {err_msg}: {e}") 

135 continue 

136 

137 if not self._is_property_ok(t, value): 

138 err_msg = f"Value calculated for property '{t}' is wrong type " \ 

139 f"({type(value)} != {self._PROPERTIES[t][1]}) using translator {translator.__class__}" \ 

140 f"{file_info}" 

141 if pedantic: 

142 raise TypeError(err_msg) 

143 else: 

144 log.debug("Calcuation of property '%s' had unexpected type with header: %s", t, header) 

145 log.warning(f"Ignoring {err_msg}") 

146 

147 setattr(self, property, value) 

148 

149 @classmethod 

150 def _is_property_ok(cls, property, value): 

151 """Compare the supplied value against the expected type as defined 

152 for the corresponding property. 

153 

154 Parameters 

155 ---------- 

156 property : `str` 

157 Name of property. 

158 value : `object` 

159 Value of the property to validate. 

160 

161 Returns 

162 ------- 

163 is_ok : `bool` 

164 `True` if the value is of an appropriate type. 

165 

166 Notes 

167 ----- 

168 Currently only the type of the property is validated. There is no 

169 attempt to check bounds or determine that a Quantity is compatible 

170 with the property. 

171 """ 

172 if value is None: 

173 return True 

174 

175 property_type = cls._PROPERTIES[property][2] 

176 

177 # For AltAz coordinates, they can either arrive as AltAz or 

178 # as SkyCoord(frame=AltAz) so try to find the frame inside 

179 # the SkyCoord. 

180 if issubclass(property_type, AltAz) and isinstance(value, SkyCoord): 

181 value = value.frame 

182 

183 if not isinstance(value, property_type): 

184 return False 

185 

186 return True 

187 

188 @property 

189 def cards_used(self): 

190 """Header cards used for the translation. 

191 

192 Returns 

193 ------- 

194 used : `frozenset` of `str` 

195 Set of card used. 

196 """ 

197 if not self._translator: 

198 return frozenset() 

199 return self._translator.cards_used() 

200 

201 def stripped_header(self): 

202 """Return a copy of the supplied header with used keywords removed. 

203 

204 Returns 

205 ------- 

206 stripped : `dict`-like 

207 Same class as header supplied to constructor, but with the 

208 headers used to calculate the generic information removed. 

209 """ 

210 hdr = copy.copy(self._header) 

211 used = self.cards_used 

212 for c in used: 

213 del hdr[c] 

214 return hdr 

215 

216 def __str__(self): 

217 # Put more interesting answers at front of list 

218 # and then do remainder 

219 priority = ("instrument", "telescope", "datetime_begin") 

220 properties = sorted(set(self._PROPERTIES.keys()) - set(priority)) 

221 

222 result = "" 

223 for p in itertools.chain(priority, properties): 

224 value = getattr(self, p) 

225 if isinstance(value, astropy.time.Time): 

226 value.format = "isot" 

227 value = str(value.value) 

228 result += f"{p}: {value}\n" 

229 

230 return result 

231 

232 def __eq__(self, other): 

233 """Compares equal if standard properties are equal 

234 """ 

235 if type(self) != type(other): 

236 return False 

237 

238 for p in self._PROPERTIES: 

239 # Use string comparison since SkyCoord.__eq__ seems unreliable 

240 # otherwise. Should have per-type code so that floats and 

241 # quantities can be compared properly. 

242 v1 = f"{getattr(self, p)}" 

243 v2 = f"{getattr(other, p)}" 

244 if v1 != v2: 

245 return False 

246 

247 return True 

248 

249 def __lt__(self, other): 

250 return self.datetime_begin < other.datetime_begin 

251 

252 def __gt__(self, other): 

253 return self.datetime_begin > other.datetime_begin 

254 

255 def __getstate__(self): 

256 """Get pickleable state 

257 

258 Returns the properties, the name of the translator, and the 

259 cards that were used. Does not return the full header. 

260 

261 Returns 

262 ------- 

263 state : `dict` 

264 Dict containing items that can be persisted. 

265 """ 

266 state = dict() 

267 for p in self._PROPERTIES: 

268 property = f"_{p}" 

269 state[p] = getattr(self, property) 

270 

271 return state 

272 

273 def __setstate__(self, state): 

274 for p in self._PROPERTIES: 

275 property = f"_{p}" 

276 setattr(self, property, state[p]) 

277 

278 @classmethod 

279 def makeObservationInfo(cls, **kwargs): # noqa: N802 

280 """Construct an `ObservationInfo` from the supplied parameters. 

281 

282 Notes 

283 ----- 

284 The supplied parameters should use names matching the property. 

285 The type of the supplied value will be checked against the property. 

286 Any properties not supplied will be assigned a value of `None`. 

287 

288 Raises 

289 ------ 

290 KeyError 

291 Raised if a supplied parameter key is not a known property. 

292 TypeError 

293 Raised if a supplied value does not match the expected type 

294 of the property. 

295 """ 

296 

297 obsinfo = cls(None) 

298 

299 unused = set(kwargs) 

300 

301 for p in cls._PROPERTIES: 

302 if p in kwargs: 

303 property = f"_{p}" 

304 value = kwargs[p] 

305 if not cls._is_property_ok(p, value): 

306 raise TypeError(f"Supplied value {value} for property {p} " 

307 f"should be of class {cls._PROPERTIES[p][1]} not {value.__class__}") 

308 setattr(obsinfo, property, value) 

309 unused.remove(p) 

310 

311 if unused: 

312 raise KeyError(f"Unrecognized properties provided: {', '.join(unused)}") 

313 

314 return obsinfo 

315 

316 

317# Method to add the standard properties 

318def _make_property(property, doc, return_typedoc, return_type): 

319 """Create a getter method with associated docstring. 

320 

321 Parameters 

322 ---------- 

323 property : `str` 

324 Name of the property getter to be created. 

325 doc : `str` 

326 Description of this property. 

327 return_typedoc : `str` 

328 Type string of this property (used in the doc string). 

329 return_type : `class` 

330 Type of this property. 

331 

332 Returns 

333 ------- 

334 p : `function` 

335 Getter method for this property. 

336 """ 

337 def getter(self): 

338 return getattr(self, f"_{property}") 

339 

340 getter.__doc__ = f"""{doc} 

341 

342 Returns 

343 ------- 

344 {property} : `{return_typedoc}` 

345 Access the property. 

346 """ 

347 return getter 

348 

349 

350# Initialize the internal properties (underscored) and add the associated 

351# getter methods. 

352for name, description in ObservationInfo._PROPERTIES.items(): 

353 setattr(ObservationInfo, f"_{name}", None) 

354 setattr(ObservationInfo, name, property(_make_property(name, *description))) 

355 

356 

357def makeObservationInfo(**kwargs): # noqa: N802 

358 """Construct an `ObservationInfo` from the supplied parameters. 

359 

360 Notes 

361 ----- 

362 The supplied parameters should use names matching the property. 

363 The type of the supplied value will be checked against the property. 

364 Any properties not supplied will be assigned a value of `None`. 

365 

366 Raises 

367 ------ 

368 KeyError 

369 Raised if a supplied parameter key is not a known property. 

370 TypeError 

371 Raised if a supplied value does not match the expected type 

372 of the property. 

373 """ 

374 return ObservationInfo.makeObservationInfo(**kwargs)