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

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.
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.
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 """
73 _PROPERTIES = PROPERTIES
74 """All the properties supported by this class with associated
75 documentation."""
77 def __init__(self, header, filename=None, translator_class=None, pedantic=False,
78 search_path=None):
80 # Initialize the empty object
81 self._header = {}
82 self.filename = filename
83 self._translator = None
84 self.translator_class_name = "<None>"
86 # To allow makeObservationInfo to work, we special case a None
87 # header
88 if header is None:
89 return
91 # Fix up the header (if required)
92 fix_header(header, translator_class=translator_class, filename=filename,
93 search_path=search_path)
95 # Store the supplied header for later stripping
96 self._header = header
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}")
103 # Create an instance for this header
104 translator = translator_class(header, filename=filename)
106 # Store the translator
107 self._translator = translator
108 self.translator_class_name = translator_class.__name__
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 = ""
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}"
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
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}")
147 setattr(self, property, value)
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.
154 Parameters
155 ----------
156 property : `str`
157 Name of property.
158 value : `object`
159 Value of the property to validate.
161 Returns
162 -------
163 is_ok : `bool`
164 `True` if the value is of an appropriate type.
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
175 property_type = cls._PROPERTIES[property][2]
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
183 if not isinstance(value, property_type):
184 return False
186 return True
188 @property
189 def cards_used(self):
190 """Header cards used for the translation.
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()
201 def stripped_header(self):
202 """Return a copy of the supplied header with used keywords removed.
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
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))
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"
230 return result
232 def __eq__(self, other):
233 """Compares equal if standard properties are equal
234 """
235 if type(self) != type(other):
236 return False
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
247 return True
249 def __lt__(self, other):
250 return self.datetime_begin < other.datetime_begin
252 def __gt__(self, other):
253 return self.datetime_begin > other.datetime_begin
255 def __getstate__(self):
256 """Get pickleable state
258 Returns the properties, the name of the translator, and the
259 cards that were used. Does not return the full header.
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)
271 return state
273 def __setstate__(self, state):
274 for p in self._PROPERTIES:
275 property = f"_{p}"
276 setattr(self, property, state[p])
278 @classmethod
279 def makeObservationInfo(cls, **kwargs): # noqa: N802
280 """Construct an `ObservationInfo` from the supplied parameters.
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`.
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 """
297 obsinfo = cls(None)
299 unused = set(kwargs)
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)
311 if unused:
312 raise KeyError(f"Unrecognized properties provided: {', '.join(unused)}")
314 return obsinfo
317# Method to add the standard properties
318def _make_property(property, doc, return_typedoc, return_type):
319 """Create a getter method with associated docstring.
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.
332 Returns
333 -------
334 p : `function`
335 Getter method for this property.
336 """
337 def getter(self):
338 return getattr(self, f"_{property}")
340 getter.__doc__ = f"""{doc}
342 Returns
343 -------
344 {property} : `{return_typedoc}`
345 Access the property.
346 """
347 return getter
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)))
357def makeObservationInfo(**kwargs): # noqa: N802
358 """Construct an `ObservationInfo` from the supplied parameters.
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`.
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)