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

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

# This file is part of astro_metadata_translator. 

# 

# Developed for the LSST Data Management System. 

# This product includes software developed by the LSST Project 

# (http://www.lsst.org). 

# See the LICENSE file at the top-level directory of this distribution 

# for details of code ownership. 

# 

# Use of this source code is governed by a 3-clause BSD-style 

# license that can be found in the LICENSE file. 

 

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

 

__all__ = ("ObservationInfo", "makeObservationInfo") 

 

import itertools 

import logging 

import copy 

 

import astropy.time 

from astropy.coordinates import SkyCoord, AltAz 

 

from .translator import MetadataTranslator 

from .properties import PROPERTIES 

from .headers import fix_header 

 

log = logging.getLogger(__name__) 

 

 

class ObservationInfo: 

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

exposure observation. 

 

Parameters 

---------- 

header : `dict`-like 

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

May be updated with header corrections if corrections are found. 

filename : `str`, optional 

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

datasets with missing header information this can sometimes 

allow for some fixups in translations. 

translator_class : `MetadataTranslator`-class, optional 

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

into standard form. Otherwise each registered translator class will 

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

pedantic : `bool`, optional 

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

individual property translations must all be implemented but can fail 

and a warning will be issued. 

search_path : iterable, optional 

Override search paths to use during header fix up. 

 

Raises 

------ 

ValueError 

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

registered translators. 

TypeError 

Raised if the supplied translator class was not a MetadataTranslator. 

KeyError 

Raised if a translation fails and pedantic mode is enabled. 

NotImplementedError 

Raised if the selected translator does not support a required 

property. 

 

Notes 

----- 

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

modify the header provided to the constructor. 

""" 

 

_PROPERTIES = PROPERTIES 

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

documentation.""" 

 

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

search_path=None): 

 

# Initialize the empty object 

self._header = {} 

self.filename = filename 

self._translator = None 

self.translator_class_name = "<None>" 

 

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

# header 

if header is None: 

return 

 

# Fix up the header (if required) 

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

search_path=search_path) 

 

# Store the supplied header for later stripping 

self._header = header 

 

# PropertyList is not dict-like so force to a dict here to simplify 

# the translation code. 

if hasattr(header, "toOrderedDict"): 

header = header.toOrderedDict() 

 

if translator_class is None: 

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

elif not issubclass(translator_class, MetadataTranslator): 

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

 

# Create an instance for this header 

translator = translator_class(header, filename=filename) 

 

# Store the translator 

self._translator = translator 

self.translator_class_name = translator_class.__name__ 

 

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

if filename: 

file_info = f" and file {filename}" 

else: 

file_info = "" 

 

# Loop over each property and request the translated form 

for t in self._PROPERTIES: 

# prototype code 

method = f"to_{t}" 

property = f"_{t}" 

 

try: 

value = getattr(translator, method)() 

except NotImplementedError as e: 

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

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

except KeyError as e: 

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

f"{file_info}" 

if pedantic: 

raise KeyError(err_msg) from e 

else: 

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

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

continue 

 

if not self._is_property_ok(t, value): 

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

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

f"{file_info}" 

if pedantic: 

raise TypeError(err_msg) 

else: 

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

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

 

setattr(self, property, value) 

 

@classmethod 

def _is_property_ok(cls, property, value): 

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

for the corresponding property. 

 

Parameters 

---------- 

property : `str` 

Name of property. 

value : `object` 

Value of the property to validate. 

 

Returns 

------- 

is_ok : `bool` 

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

 

Notes 

----- 

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

attempt to check bounds or determine that a Quantity is compatible 

with the property. 

""" 

if value is None: 

return True 

 

property_type = cls._PROPERTIES[property][2] 

 

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

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

# the SkyCoord. 

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

value = value.frame 

 

if not isinstance(value, property_type): 

return False 

 

return True 

 

@property 

def cards_used(self): 

"""Header cards used for the translation. 

 

Returns 

------- 

used : `frozenset` of `str` 

Set of card used. 

""" 

if not self._translator: 

return frozenset() 

return self._translator.cards_used() 

 

def stripped_header(self): 

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

 

Returns 

------- 

stripped : `dict`-like 

Same class as header supplied to constructor, but with the 

headers used to calculate the generic information removed. 

""" 

hdr = copy.copy(self._header) 

used = self.cards_used 

for c in used: 

del hdr[c] 

return hdr 

 

def __str__(self): 

# Put more interesting answers at front of list 

# and then do remainder 

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

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

 

result = "" 

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

value = getattr(self, p) 

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

value.format = "isot" 

value = str(value.value) 

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

 

return result 

 

def __eq__(self, other): 

"""Compares equal if standard properties are equal 

""" 

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

return False 

 

for p in self._PROPERTIES: 

# Use string comparison since SkyCoord.__eq__ seems unreliable 

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

# quantities can be compared properly. 

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

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

if v1 != v2: 

return False 

 

return True 

 

def __lt__(self, other): 

return self.datetime_begin < other.datetime_begin 

 

def __gt__(self, other): 

return self.datetime_begin > other.datetime_begin 

 

def __getstate__(self): 

"""Get pickleable state 

 

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

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

 

Returns 

------- 

state : `dict` 

Dict containing items that can be persisted. 

""" 

state = dict() 

for p in self._PROPERTIES: 

property = f"_{p}" 

state[p] = getattr(self, property) 

 

return state 

 

def __setstate__(self, state): 

for p in self._PROPERTIES: 

property = f"_{p}" 

setattr(self, property, state[p]) 

 

@classmethod 

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

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

 

Notes 

----- 

The supplied parameters should use names matching the property. 

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

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

 

Raises 

------ 

KeyError 

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

TypeError 

Raised if a supplied value does not match the expected type 

of the property. 

""" 

 

obsinfo = cls(None) 

 

unused = set(kwargs) 

 

for p in cls._PROPERTIES: 

if p in kwargs: 

property = f"_{p}" 

value = kwargs[p] 

if not cls._is_property_ok(p, value): 

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

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

setattr(obsinfo, property, value) 

unused.remove(p) 

 

if unused: 

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

 

return obsinfo 

 

 

# Method to add the standard properties 

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

"""Create a getter method with associated docstring. 

 

Parameters 

---------- 

property : `str` 

Name of the property getter to be created. 

doc : `str` 

Description of this property. 

return_typedoc : `str` 

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

return_type : `class` 

Type of this property. 

 

Returns 

------- 

p : `function` 

Getter method for this property. 

""" 

def getter(self): 

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

 

getter.__doc__ = f"""{doc} 

 

Returns 

------- 

{property} : `{return_typedoc}` 

Access the property. 

""" 

return getter 

 

 

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

# getter methods. 

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

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

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

 

 

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

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

 

Notes 

----- 

The supplied parameters should use names matching the property. 

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

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

 

Raises 

------ 

KeyError 

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

TypeError 

Raised if a supplied value does not match the expected type 

of the property. 

""" 

return ObservationInfo.makeObservationInfo(**kwargs)