Coverage for python/astro_metadata_translator/observationInfo.py : 18%

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.
12"""Represent standard metadata from instrument headers"""
14__all__ = ("ObservationInfo", "makeObservationInfo")
16import itertools
17import logging
18import copy
20import astropy.time
21from astropy.coordinates import SkyCoord, AltAz
23from .translator import MetadataTranslator
24from .properties import PROPERTIES
25from .headers import fix_header
27log = logging.getLogger(__name__)
30class ObservationInfo:
31 """Standardized representation of an instrument header for a single
32 exposure observation.
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).
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.
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 """
86 _PROPERTIES = PROPERTIES
87 """All the properties supported by this class with associated
88 documentation."""
90 def __init__(self, header, filename=None, translator_class=None, pedantic=False,
91 search_path=None, required=None, subset=None):
93 # Initialize the empty object
94 self._header = {}
95 self.filename = filename
96 self._translator = None
97 self.translator_class_name = "<None>"
99 # To allow makeObservationInfo to work, we special case a None
100 # header
101 if header is None:
102 return
104 # Fix up the header (if required)
105 fix_header(header, translator_class=translator_class, filename=filename,
106 search_path=search_path)
108 # Store the supplied header for later stripping
109 self._header = header
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}")
116 # Create an instance for this header
117 translator = translator_class(header, filename=filename)
119 # Store the translator
120 self._translator = translator
121 self.translator_class_name = translator_class.__name__
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 = ""
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
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}")
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}"
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
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}")
179 if value is None and t in required:
180 raise KeyError(f"Calculation of required property {t} resulted in a value of None")
182 setattr(self, property, value)
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.
189 Parameters
190 ----------
191 property : `str`
192 Name of property.
193 value : `object`
194 Value of the property to validate.
196 Returns
197 -------
198 is_ok : `bool`
199 `True` if the value is of an appropriate type.
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
210 property_type = cls._PROPERTIES[property][2]
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
218 if not isinstance(value, property_type):
219 return False
221 return True
223 @property
224 def cards_used(self):
225 """Header cards used for the translation.
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()
236 def stripped_header(self):
237 """Return a copy of the supplied header with used keywords removed.
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
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))
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"
265 return result
267 def __eq__(self, other):
268 """Compares equal if standard properties are equal
269 """
270 if type(self) != type(other):
271 return False
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
282 return True
284 def __lt__(self, other):
285 return self.datetime_begin < other.datetime_begin
287 def __gt__(self, other):
288 return self.datetime_begin > other.datetime_begin
290 def __getstate__(self):
291 """Get pickleable state
293 Returns the properties, the name of the translator, and the
294 cards that were used. Does not return the full header.
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)
306 return state
308 def __setstate__(self, state):
309 for p in self._PROPERTIES:
310 property = f"_{p}"
311 setattr(self, property, state[p])
313 @classmethod
314 def makeObservationInfo(cls, **kwargs): # noqa: N802
315 """Construct an `ObservationInfo` from the supplied parameters.
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`.
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 """
332 obsinfo = cls(None)
334 unused = set(kwargs)
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)
346 if unused:
347 raise KeyError(f"Unrecognized properties provided: {', '.join(unused)}")
349 return obsinfo
352# Method to add the standard properties
353def _make_property(property, doc, return_typedoc, return_type):
354 """Create a getter method with associated docstring.
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.
367 Returns
368 -------
369 p : `function`
370 Getter method for this property.
371 """
372 def getter(self):
373 return getattr(self, f"_{property}")
375 getter.__doc__ = f"""{doc}
377 Returns
378 -------
379 {property} : `{return_typedoc}`
380 Access the property.
381 """
382 return getter
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)))
392def makeObservationInfo(**kwargs): # noqa: N802
393 """Construct an `ObservationInfo` from the supplied parameters.
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`.
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)