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 required : `set`, optional 

54 This parameter can be used to confirm that all properties contained 

55 in the set must translate correctly and also be non-None. For the case 

56 where ``pedantic`` is `True` this will still check that the resulting 

57 value is not `None`. 

58 subset : `set`, optional 

59 If not `None`, controls the translations that will be performed 

60 during construction. This can be useful if the caller is only 

61 interested in a subset of the properties and knows that some of 

62 the others might be slow to compute (for example the airmass if it 

63 has to be derived). 

64 

65 Raises 

66 ------ 

67 ValueError 

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

69 registered translators. Also raised if the request property subset 

70 is not a subset of the known properties. 

71 TypeError 

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

73 KeyError 

74 Raised if a required property cannot be calculated, or if pedantic 

75 mode is enabled and any translations fails. 

76 NotImplementedError 

77 Raised if the selected translator does not support a required 

78 property. 

79 

80 Notes 

81 ----- 

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

83 modify the header provided to the constructor. 

84 """ 

85 

86 _PROPERTIES = PROPERTIES 

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

88 documentation.""" 

89 

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

91 search_path=None, required=None, subset=None): 

92 

93 # Initialize the empty object 

94 self._header = {} 

95 self.filename = filename 

96 self._translator = None 

97 self.translator_class_name = "<None>" 

98 

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

100 # header 

101 if header is None: 

102 return 

103 

104 # Fix up the header (if required) 

105 fix_header(header, translator_class=translator_class, filename=filename, 

106 search_path=search_path) 

107 

108 # Store the supplied header for later stripping 

109 self._header = header 

110 

111 if translator_class is None: 

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

113 elif not issubclass(translator_class, MetadataTranslator): 

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

115 

116 # Create an instance for this header 

117 translator = translator_class(header, filename=filename) 

118 

119 # Store the translator 

120 self._translator = translator 

121 self.translator_class_name = translator_class.__name__ 

122 

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

124 if filename: 

125 file_info = f" and file {filename}" 

126 else: 

127 file_info = "" 

128 

129 # Determine the properties of interest 

130 all_properties = set(self._PROPERTIES) 

131 if subset is not None: 

132 if not subset: 

133 raise ValueError("Cannot request no properties be calculated.") 

134 if not subset.issubset(all_properties): 

135 raise ValueError("Requested subset is not a subset of known properties. " 

136 f"Got extra: {subset - all_properties}") 

137 properties = subset 

138 else: 

139 properties = all_properties 

140 

141 if required is None: 

142 required = set() 

143 else: 

144 if not required.issubset(all_properties): 

145 raise ValueError("Requested required properties include unknowns: " 

146 f"{required - all_properties}") 

147 

148 # Loop over each property and request the translated form 

149 for t in properties: 

150 # prototype code 

151 method = f"to_{t}" 

152 property = f"_{t}" 

153 

154 try: 

155 value = getattr(translator, method)() 

156 except NotImplementedError as e: 

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

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

159 except KeyError as e: 

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

161 f"{file_info}" 

162 if pedantic or t in required: 

163 raise KeyError(err_msg) from e 

164 else: 

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

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

167 continue 

168 

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

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

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

172 f"{file_info}" 

173 if pedantic or t in required: 

174 raise TypeError(err_msg) 

175 else: 

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

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

178 

179 if value is None and t in required: 

180 raise KeyError(f"Calculation of required property {t} resulted in a value of None") 

181 

182 setattr(self, property, value) 

183 

184 @classmethod 

185 def _is_property_ok(cls, property, value): 

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

187 for the corresponding property. 

188 

189 Parameters 

190 ---------- 

191 property : `str` 

192 Name of property. 

193 value : `object` 

194 Value of the property to validate. 

195 

196 Returns 

197 ------- 

198 is_ok : `bool` 

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

200 

201 Notes 

202 ----- 

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

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

205 with the property. 

206 """ 

207 if value is None: 

208 return True 

209 

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

211 

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

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

214 # the SkyCoord. 

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

216 value = value.frame 

217 

218 if not isinstance(value, property_type): 

219 return False 

220 

221 return True 

222 

223 @property 

224 def cards_used(self): 

225 """Header cards used for the translation. 

226 

227 Returns 

228 ------- 

229 used : `frozenset` of `str` 

230 Set of card used. 

231 """ 

232 if not self._translator: 

233 return frozenset() 

234 return self._translator.cards_used() 

235 

236 def stripped_header(self): 

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

238 

239 Returns 

240 ------- 

241 stripped : `dict`-like 

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

243 headers used to calculate the generic information removed. 

244 """ 

245 hdr = copy.copy(self._header) 

246 used = self.cards_used 

247 for c in used: 

248 del hdr[c] 

249 return hdr 

250 

251 def __str__(self): 

252 # Put more interesting answers at front of list 

253 # and then do remainder 

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

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

256 

257 result = "" 

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

259 value = getattr(self, p) 

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

261 value.format = "isot" 

262 value = str(value.value) 

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

264 

265 return result 

266 

267 def __eq__(self, other): 

268 """Compares equal if standard properties are equal 

269 """ 

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

271 return False 

272 

273 for p in self._PROPERTIES: 

274 # Use string comparison since SkyCoord.__eq__ seems unreliable 

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

276 # quantities can be compared properly. 

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

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

279 if v1 != v2: 

280 return False 

281 

282 return True 

283 

284 def __lt__(self, other): 

285 return self.datetime_begin < other.datetime_begin 

286 

287 def __gt__(self, other): 

288 return self.datetime_begin > other.datetime_begin 

289 

290 def __getstate__(self): 

291 """Get pickleable state 

292 

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

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

295 

296 Returns 

297 ------- 

298 state : `dict` 

299 Dict containing items that can be persisted. 

300 """ 

301 state = dict() 

302 for p in self._PROPERTIES: 

303 property = f"_{p}" 

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

305 

306 return state 

307 

308 def __setstate__(self, state): 

309 for p in self._PROPERTIES: 

310 property = f"_{p}" 

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

312 

313 @classmethod 

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

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

316 

317 Notes 

318 ----- 

319 The supplied parameters should use names matching the property. 

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

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

322 

323 Raises 

324 ------ 

325 KeyError 

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

327 TypeError 

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

329 of the property. 

330 """ 

331 

332 obsinfo = cls(None) 

333 

334 unused = set(kwargs) 

335 

336 for p in cls._PROPERTIES: 

337 if p in kwargs: 

338 property = f"_{p}" 

339 value = kwargs[p] 

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

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

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

343 setattr(obsinfo, property, value) 

344 unused.remove(p) 

345 

346 if unused: 

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

348 

349 return obsinfo 

350 

351 

352# Method to add the standard properties 

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

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

355 

356 Parameters 

357 ---------- 

358 property : `str` 

359 Name of the property getter to be created. 

360 doc : `str` 

361 Description of this property. 

362 return_typedoc : `str` 

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

364 return_type : `class` 

365 Type of this property. 

366 

367 Returns 

368 ------- 

369 p : `function` 

370 Getter method for this property. 

371 """ 

372 def getter(self): 

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

374 

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

376 

377 Returns 

378 ------- 

379 {property} : `{return_typedoc}` 

380 Access the property. 

381 """ 

382 return getter 

383 

384 

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

386# getter methods. 

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

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

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

390 

391 

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

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

394 

395 Notes 

396 ----- 

397 The supplied parameters should use names matching the property. 

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

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

400 

401 Raises 

402 ------ 

403 KeyError 

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

405 TypeError 

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

407 of the property. 

408 """ 

409 return ObservationInfo.makeObservationInfo(**kwargs)