# See COPYRIGHT file at the top of the source tree.
from __future__ import print_function, division
import numpy as np
import astropy.units as u
from .jsonmixin import JsonSerializationMixin
__all__ = ['Datum', 'QuantityAttributeMixin']
[docs]class QuantityAttributeMixin(object):
"""Mixin with common attributes for classes that wrap an
`astropy.units.Quantity`.
Subclasses must have a self._quantity attribute that is an
`astropy.units.Quantity`, `str`, `bool`, or `None` (only numeric values are
astropy quantities).
"""
@property
def quantity(self):
"""Value of the datum (`astropy.units.Quantity`, `str`, `bool`,
`None`)."""
return self._quantity
@quantity.setter
def quantity(self, q):
assert isinstance(q, u.Quantity) or \
isinstance(q, basestring) or isinstance(q, bool) or \
isinstance(q, int) or q is None
self._quantity = q
@property
def unit(self):
"""Read-only `astropy.units.Unit` of the `quantity`.
If the `quantity` is a `str` or `bool`, the unit is `None`.
"""
q = self.quantity
if q is None or isinstance(q, basestring) or isinstance(q, bool) or \
isinstance(q, int):
return None
else:
return q.unit
@property
def unit_str(self):
"""Read-only `astropy.units.Unit`-compatible `str` indicating units of
`quantity`.
"""
if self.unit is None:
# unitless quantites have an empty string for a unit; retain this
# behaviour for str and bool quantities.
return ''
else:
return str(self.unit)
@property
def latex_unit(self):
"""Units as a LaTeX string, wrapped in ``$``."""
if self.unit is not None and self.unit != '':
fmtr = u.format.Latex()
return fmtr.to_string(self.unit)
else:
return ''
@staticmethod
def _rebuild_quantity(value, unit):
"""Rebuild a quantity from the value and unit serialized to JSON.
Parameters
----------
value : `list`, `float`, `int`, `str`, `bool`
Serialized quantity value.
unit : `str`
Serialized quantity unit string.
Returns
-------
q : `astropy.units.Quantity`, `str`, `int`, `bool` or `None`
Astropy quantity.
"""
if isinstance(value, basestring) or \
isinstance(value, bool) or \
isinstance(value, int) or \
value is None:
# a str or bool
_quantity = value
elif isinstance(value, list):
# an astropy quantity array
_quantity = np.array(value) * u.Unit(unit)
else:
# scalar astropy quantity
_quantity = value * u.Unit(unit)
return _quantity
[docs]class Datum(QuantityAttributeMixin, JsonSerializationMixin):
"""A value annotated with units, a plot label and description.
Datum supports natively support Astropy `~astropy.units.Quantity` and
units. In addition, a Datum can also wrap strings, booleans and integers.
A Datums's value can also be `None`.
Parameters
----------
quantity : `astropy.units.Quantity`, `int`, `float` or iterable.
Value of the `Datum`.
unit : `str`
Units of ``quantity`` as a `str` if ``quantity`` is not supplied as an
`astropy.units.Quantity`. See http://docs.astropy.org/en/stable/units/.
Units are not used by `str`, `bool`, `int` or `None` types.
label : `str`, optional
Label suitable for plot axes (without units).
description : `str`, optional
Extended description of the `Datum`.
"""
def __init__(self, quantity=None, unit=None, label=None, description=None):
self._label = None
self._description = None
self.label = label
self.description = description
self._quantity = None
if quantity is None:
self._quantity = None
elif isinstance(quantity, u.Quantity) or \
isinstance(quantity, basestring) or \
isinstance(quantity, bool) or isinstance(quantity, int):
self.quantity = quantity
elif unit is not None:
self.quantity = u.Quantity(quantity, unit=unit)
else:
raise ValueError('`unit` argument must be supplied to Datum '
'if `quantity` is not an astropy.unit.Quantity.')
@classmethod
[docs] def from_json(cls, json_data):
"""Construct a Datum from a JSON dataset.
Parameters
----------
json_data : `dict`
Datum JSON object.
Returns
-------
datum : `Datum`
Datum from JSON.
"""
q = Datum._rebuild_quantity(json_data['value'], json_data['unit'])
d = cls(quantity=q, label=json_data['label'],
description=json_data['description'])
return d
@property
def json(self):
"""Datum as a `dict` compatible with overall `Job` JSON schema."""
if self.quantity is None:
v = None
elif isinstance(self.quantity, basestring) or \
isinstance(self.quantity, int) or \
isinstance(self.quantity, bool):
v = self.quantity
elif len(self.quantity.shape) > 0:
v = self.quantity.value.tolist()
else:
v = self.quantity.value
d = {
'value': v,
'unit': self.unit_str,
'label': self.label,
'description': self.description
}
return d
@property
def label(self):
"""Label for plotting (without units)."""
return self._label
@label.setter
def label(self, value):
assert isinstance(value, basestring) or value is None
self._label = value
@property
def description(self):
"""Extended description."""
return self._description
@description.setter
def description(self, value):
assert isinstance(value, basestring) or value is None
self._description = value