Skip to content

Commit

Permalink
Added cfunits units backend
Browse files Browse the repository at this point in the history
  • Loading branch information
mcgibbon committed Aug 21, 2018
1 parent fd62cc0 commit ec02d71
Show file tree
Hide file tree
Showing 10 changed files with 272 additions and 40 deletions.
8 changes: 8 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
What's New
==========


Latest
------

* Added set_units_backend function with new cfunits backend which is used by default
when cf_units is available.


v0.4.0
------

Expand Down
3 changes: 2 additions & 1 deletion sympl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
get_component_aliases)
from sympl._core.combine_properties import combine_component_properties
from ._core.units import units_are_same, units_are_compatible, is_valid_unit
from ._core.units import set_backend as set_units_backend
from sympl._core.get_np_arrays import get_numpy_arrays_with_properties
from sympl._core.restore_dataarray import restore_data_arrays_with_properties
from sympl._core.init_np_arrays import initialize_numpy_arrays_with_properties
Expand All @@ -42,7 +43,7 @@
InvalidStateError, SharedKeyError, DependencyError,
InvalidPropertyDictError, ComponentExtraOutputError,
ComponentMissingOutputError,
units_are_same, units_are_compatible, is_valid_unit,
units_are_same, units_are_compatible, is_valid_unit, set_units_backend,
DataArray,
get_constant, set_constant, set_condensible_name, reset_constants,
get_constants_string, TimeDifferencingWrapper,
Expand Down
4 changes: 2 additions & 2 deletions sympl/_components/basic.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .._core.dataarray import DataArray
from .._core.base_components import ImplicitTendencyComponent, TendencyComponent, DiagnosticComponent
from .._core.units import unit_registry as ureg
from .._core.units import clean_units


class ConstantTendencyComponent(TendencyComponent):
Expand Down Expand Up @@ -281,7 +281,7 @@ def tendency_properties(self):
return {
self._quantity_name: {
'dims': ['*'],
'units': str(ureg(self._units) / ureg('s')),
'units': clean_units('{} / s'.format(self._units)),
}
}

Expand Down
4 changes: 2 additions & 2 deletions sympl/_components/netcdf.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from .._core.base_components import Monitor
from .._core.exceptions import (
DependencyError, InvalidStateError)
from .._core.units import from_unit_to_another
from .._core.units import array_from_units_to_another
from .._core.dataarray import DataArray
from .._core.util import same_list, datetime64_to_datetime
import xarray as xr
Expand Down Expand Up @@ -263,7 +263,7 @@ def append_times_to_dataset(times, dataset, time_units):
times_list = []
for time in times:
times_list.append(time.total_seconds())
time_array = from_unit_to_another(
time_array = array_from_units_to_another(
np.array(times_list), 'seconds', time_units)
dataset.variables['time'][it_start:it_end] = time_array[:]
else: # assume datetime
Expand Down
10 changes: 8 additions & 2 deletions sympl/_core/dataarray.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import xarray as xr
from pint.errors import DimensionalityError
from .units import data_array_to_units as to_units_function
from .units import data_array_to_array_with_units


class DataArray(xr.DataArray):
Expand Down Expand Up @@ -47,6 +47,12 @@ def to_units(self, units):
if 'units' not in self.attrs:
raise KeyError('"units" not present in attrs')
try:
return to_units_function(self, units)
out_array = data_array_to_array_with_units(self, units)
out_attrs = {}
out_attrs.update(self.attrs)
out_attrs['units'] = units
return DataArray(
out_array, dims=self.dims, coords=self.coords, attrs=out_attrs
)
except DimensionalityError as err:
raise ValueError(str(err))
4 changes: 4 additions & 0 deletions sympl/_core/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
class UnitError(Exception):
pass


class InvalidStateError(Exception):
pass

Expand Down
79 changes: 79 additions & 0 deletions sympl/_core/units/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from ._pint import (
backend_units_are_same, backend_is_valid_unit, backend_clean_units,
backend_array_from_units_to_another
)
from . import _cfunits
from ..exceptions import UnitError
import numpy as np


def units_are_same(unit1, unit2):
return backend_units_are_same(unit1, unit2)


def is_valid_unit(unit_string):
return backend_is_valid_unit(unit_string)


def clean_units(unit_string):
return backend_clean_units(unit_string)


def array_from_units_to_another(value, original_units, new_units):
return backend_array_from_units_to_another(value, original_units, new_units)


def units_are_compatible(unit1, unit2):
"""
Determine whether a unit can be converted to another unit.
Parameters
----------
unit1 : str
unit2 : str
Returns
-------
units_are_compatible : bool
True if the first unit can be converted to the second unit.
"""
try:
array_from_units_to_another(np.array(1.), unit1, unit2)
except UnitError:
return False
return True


def data_array_to_array_with_units(data_array, out_units):
if not hasattr(data_array, 'attrs') or 'units' not in data_array.attrs:
raise TypeError(
'Cannot retrieve units from type {}'.format(type(data_array)))
elif units_are_same(data_array.attrs['units'], out_units):
out_array = data_array.values
elif not units_are_compatible(data_array.attrs['units'], out_units):
raise UnitError(
'Units {} and {} are incompatible'.format(
data_array.attrs['units'], out_units)
)
else:
out_array = array_from_units_to_another(
data_array.values, data_array.attrs['units'], out_units)
return out_array


def set_backend(backend_name):
global backend_units_are_same, backend_is_valid_unit, backend_clean_units
global backend_array_from_units_to_another
if backend_name.lower() == 'pint':
from ._pint import (
backend_units_are_same, backend_is_valid_unit, backend_clean_units,
backend_array_from_units_to_another
)
elif backend_name.lower() in ('cfunits', 'cf-units', 'cf_units'):
from ._cfunits import (
backend_units_are_same, backend_is_valid_unit, backend_clean_units,
backend_array_from_units_to_another
)

if _cfunits.cf_units is not None:
set_backend('cfunits')
45 changes: 45 additions & 0 deletions sympl/_core/units/_cfunits.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from cf_units import Unit
try:
import cf_units
except ImportError:
cf_units = None
from ..exceptions import UnitError


def backend_units_are_same(unit1, unit2):
"""
Compare two unit strings for equality.
Parameters
----------
unit1 : str
unit2 : str
Returns
-------
units_are_same : bool
True if the two input unit strings represent the same unit.
"""
return Unit(unit1) == Unit(unit2)


def backend_clean_units(unit_string):
return Unit(unit_string).format(cf_units.UT_NAMES)


def backend_is_valid_unit(unit_string):
"""Returns True if the unit string is recognized, and False otherwise."""
try:
Unit(unit_string)
except ValueError:
return False
return True


def backend_array_from_units_to_another(value, original_units, new_units):
try:
return Unit(original_units).convert(value, Unit(new_units))
except ValueError:
raise UnitError('units {} and {} are incompatible'.format(
original_units, new_units)
)
25 changes: 8 additions & 17 deletions sympl/_core/units.py → sympl/_core/units/_pint.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ def __call__(self, input_string, **kwargs):
unit_registry = UnitRegistry()
unit_registry.define('degrees_north = degree_north = degree_N = degrees_N = degreeN = degreesN')
unit_registry.define('degrees_east = degree_east = degree_E = degrees_E = degreeE = degreesE')
unit_registry.define('degrees_Celsius = degC')
unit_registry.define('degree_Celsius = degC')
unit_registry.define('percent = 0.01*count = %')


def units_are_compatible(unit1, unit2):
def backend_units_are_compatible(unit1, unit2):
"""
Determine whether a unit can be converted to another unit.
Expand All @@ -40,7 +42,7 @@ def units_are_compatible(unit1, unit2):
return False


def units_are_same(unit1, unit2):
def backend_units_are_same(unit1, unit2):
"""
Compare two unit strings for equality.
Expand All @@ -57,11 +59,11 @@ def units_are_same(unit1, unit2):
return unit_registry(unit1) == unit_registry(unit2)


def clean_units(unit_string):
def backend_clean_units(unit_string):
return str(unit_registry(unit_string).to_base_units().units)


def is_valid_unit(unit_string):
def backend_is_valid_unit(unit_string):
"""Returns True if the unit string is recognized, and False otherwise."""
unit_string = unit_string.replace(
'%', 'percent').replace(
Expand All @@ -74,17 +76,6 @@ def is_valid_unit(unit_string):
return True


def data_array_to_units(value, units):
if not hasattr(value, 'attrs') or 'units' not in value.attrs:
raise TypeError(
'Cannot retrieve units from type {}'.format(type(value)))
elif unit_registry(value.attrs['units']) != unit_registry(units):
attrs = value.attrs.copy()
value = unit_registry.Quantity(value, value.attrs['units']).to(units).magnitude
attrs['units'] = units
value.attrs = attrs
return value
def backend_array_from_units_to_another(value, original_units, new_units):
return unit_registry.Quantity(value, original_units).to(new_units).magnitude


def from_unit_to_another(value, original_units, new_units):
return (unit_registry(original_units)*value).to(new_units).magnitude

0 comments on commit ec02d71

Please sign in to comment.