python4oceanographers

Turning ripples into waves

Yet another units module

I have been working in splitting iris units module into its own module for a while and recently we had our first release, and our first external use: the IOOS compliance-checker.

(Warning! Before reading the post please watch the video below.)

In [2]:
from IPython.display import YouTubeVideo

YouTubeVideo("N-edLdxiM40")
Out[2]:

cf_units goal is to be a CF-compliant and UDUNITS-compatible units module.

Until now the next best was udunitspy. Sadly udunitspy is no longer being developed, nor it works on Windows.

In this post I will make a comparison with udunitspy to make the case for replacing with cf_units and, hopefully, present a quick introduction on how to use cf_units.

The Unit object

In [3]:
import udunitspy

upy = udunitspy.Unit('m/s')
print('{}: {!r}'.format(upy, upy))
m.s-1: <units of 'm.s-1'>

In [4]:
from cf_units import cf_units

uir = cf_units.Unit('m/s')
print('{}: {!r}'.format(uir, uir))
m/s: Unit('m/s')

Units known/unknown

In [5]:
right = 'm/s'
wrong = 'coconuts'


def units_known(key):
    try:
        udunitspy.Unit(str(key))
    except udunitspy.UdunitsError:
        return False
    return True

units_known(right), units_known(wrong)
Out[5]:
(True, False)
In [6]:
def units_known(key):
    try:
        cf_units.Unit(key)
    except ValueError:  # I prefer standard exceptions.
        return False
    return True

units_known(right), units_known(wrong)
Out[6]:
(True, False)

Can it quack like a duck?

In [7]:
cf_units.Unit(cf_units.Unit('m/s'))  # OK.
Out[7]:
Unit('m/s')
In [8]:
udunitspy.Unit(udunitspy.Unit('m/s'))  # Nope.  Only strings!
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-8-b752a337c5dd> in <module>()
----> 1 udunitspy.Unit(udunitspy.Unit('m/s'))  # Nope.  Only strings!

/home/filipe/.virtualenvs/blog/lib/python2.7/site-packages/udunitspy/udunits2.py in __init__(self, spec, system, encoding)
    158             system = System(path=system)
    159         self.system = system or DEFAULT_SYSTEM
--> 160         self.this = ut.parse(self.system.this, spec, encoding or UT_ASCII)
    161 
    162         if not self.this:

TypeError: in method 'parse', argument 2 of type 'char const *const'

Units convertible?

In [9]:
def units_convertible(units1, units2, reftimeistime=True):
    """Return True if a Unit representing the string units1 can be converted
    to a Unit representing the string units2, else False."""
    try:
        udunitspy.Converter(str(units1), str(units2))
    except udunitspy.UdunitsError:
        return False

    u1 = udunitspy.Unit(str(units1))
    u2 = udunitspy.Unit(str(units2))
    return u1.are_convertible(u2)

units_convertible('km/h', 'm/s'), units_convertible('km/h', 's')
Out[9]:
(True, False)
In [10]:
def units_convertible(units1, units2):
    """No need for a try/exception clause."""
    u1 = cf_units.Unit(units1)
    return u1.is_convertible(units2)


units_convertible('km/h', 'm/s'), units_convertible('km/h', 's')
Out[10]:
(True, False)

Temporal units

In [11]:
def units_temporal(units):
    r = False
    try:
        u = udunitspy.Unit('seconds since 1900-01-01')
        r = u.are_convertible(str(units))
    except udunitspy.UdunitsError:
        return False
    return r

units_temporal('seconds since 1900-01-01')
Out[11]:
True
In [12]:
def units_temporal(key):
    return cf_units.Unit(key).is_time_reference()

units_temporal('seconds since 1900-01-01')
Out[12]:
True

... many more is_<something> are available in cf_units

In [13]:
t = cf_units.Unit('seconds since 1900-01-01')

(t.is_udunits(), t.is_dimensionless(), t.is_no_unit(),
 t.is_unknown(), t.is_vertical(), t.is_time())
Out[13]:
(True, False, False, False, False, False)

The exceptions

In [14]:
udunitspy.Unit(wrong)  # Custom exception.
---------------------------------------------------------------------------
UdunitsError                              Traceback (most recent call last)
<ipython-input-14-6c7e6518afc6> in <module>()
----> 1 udunitspy.Unit(wrong)  # Custom exception.

/home/filipe/.virtualenvs/blog/lib/python2.7/site-packages/udunitspy/udunits2.py in __init__(self, spec, system, encoding)
    161 
    162         if not self.this:
--> 163             raise UdunitsError(Unit.__init__.__name__, ut.get_status())
    164 
    165     def copy(self):

UdunitsError: __init__ resulted in udunits error UT_UNKNOWN:  String unit representation contains unknown word
In [15]:
cf_units.Unit(wrong)  # Regular ValueError!
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-15-57d41d544bd7> in <module>()
----> 1 cf_units.Unit(wrong)  # Regular ValueError!

/home/filipe/.virtualenvs/blog/lib/python2.7/site-packages/cf_units/cf_units.py in __init__(self, unit, calendar)
    928             # _ut_parse returns 0 on failure
    929             if ut_unit is None:
--> 930                 self._raise_error('Failed to parse unit "%s"' % unit)
    931             if _OP_SINCE in unit.lower():
    932                 if calendar is None:

/home/filipe/.virtualenvs/blog/lib/python2.7/site-packages/cf_units/cf_units.py in _raise_error(self, msg)
    963                 ctypes.set_errno(0)
    964 
--> 965         raise ValueError('[%s] %s %s' % (status_msg, msg, error_msg))
    966 
    967     # NOTE:

ValueError: [UT_UNKNOWN] Failed to parse unit "coconuts" 

The cf_units.Units object is richer in methods and properties

In [16]:
a = cf_units.Unit('km/h')

a.title(42), a.symbol, a.definition, a.origin
Out[16]:
('42 km/h', '0.277777777777778 m.s-1', '0.277777777777778 m.s-1', 'km/h')
In [17]:
formats = (cf_units.UT_ASCII, cf_units.UT_ISO_8859_1, cf_units.UT_LATIN1,
           cf_units.UT_UTF8, cf_units.UT_NAMES, cf_units.UT_DEFINITION)

for fmt in formats:
    print(a.format(option=fmt))
0.277777777777778 m.s-1
0.277777777777778 m/s
0.277777777777778 m/s
0.277777777777778 m·s⁻¹
0.277777777777778 meter-second^-1
0.277777777777778 m.s-1

Some are units specific properties

In [18]:
a = cf_units.Unit('degree')

a.modulus
Out[18]:
360.0

How about creating time-ranges?

In [19]:
u = cf_units.Unit('hours since 1970-01-01 00:00:00',
                  calendar=cf_units.CALENDAR_STANDARD)
ut = u.utime()
ut.num2date(range(10))
Out[19]:
array([datetime.datetime(1970, 1, 1, 0, 0),
       datetime.datetime(1970, 1, 1, 1, 0),
       datetime.datetime(1970, 1, 1, 2, 0, 0, 13),
       datetime.datetime(1970, 1, 1, 3, 0),
       datetime.datetime(1970, 1, 1, 4, 0),
       datetime.datetime(1970, 1, 1, 5, 0, 0, 13),
       datetime.datetime(1970, 1, 1, 6, 0),
       datetime.datetime(1970, 1, 1, 7, 0),
       datetime.datetime(1970, 1, 1, 8, 0, 0, 13),
       datetime.datetime(1970, 1, 1, 9, 0)], dtype=object)

And time conversions?

In [20]:
import numpy as np

var = [1, 2, 3, 4]

udunitspy.Unit('hours since 1970-01-01 00:00:00').get_converter("seconds since 1970-01-01").evaluate(min(var))
Out[20]:
3600.0
In [21]:
calendar = cf_units.CALENDAR_GREGORIAN

origin = cf_units.Unit('hours since 1970-01-01 00:00:00', calendar=calendar)
target = cf_units.Unit('seconds since 1970-01-01', calendar=calendar)

origin.convert(min(var), target)
Out[21]:
3600.0

I hope to write cf_units docs soon! Stay tuned.

In [22]:
HTML(html)
Out[22]:

This post was written as an IPython notebook. It is available for download or as a static html.

Creative Commons License
python4oceanographers by Filipe Fernandes is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
Based on a work at https://ocefpaf.github.io/.

Comments