natu Tutorial

This tutorial shows how to use the natu module to work with physical quantities in Python.

Before we get started, we'll set the display precision for this IPython notebook.

In [1]:
%precision 4
Out[1]:
u'%.4f'

Basic use

Entering and displaying a quantity

First, we'll import the metre (m):

In [2]:
from natu.units import m
m
Out[2]:
ScalarUnit m with dimension L (prefixable)

It's a scalar unit that displays as "m". Its dimension is length (L) and it can be used with SI prefixes.

To enter a quantity, we multiply a number by a unit:

In [3]:
length = 0.0254*m
length
Out[3]:
0.0254 m

We can change the display unit by setting the quantity's display property:

In [4]:
length.display_unit = 'inch'
length
Out[4]:
0.0254 m

We can also express the length as a number of inches by dividing it by the inch:

In [5]:
from natu.units import inch
length/inch
Out[5]:
1.0000

Prefixed units

We saw that the metre is prefixable. Let's import the kilometre:

In [6]:
from natu.units import km
km
Out[6]:
ScalarUnit km with dimension L (not prefixable)

It can't be prefixed any further. As expected, it's 1000 times larger than the metre:

In [7]:
km/m
Out[7]:
1000.0000

We can access units directly from the units module, with or without prefixes:

In [8]:
from natu import units as U
U.km/U.m
Out[8]:
1000.0000

However, if we use a wildcard import (from natu.units import *), we only get the units which are explicitly given in the the definition files. Typically this doesn't include prefixed units.

Nonscalar units

Nonscalar units such as the degree Celsius, degree Fahrenheit, and decibel are available:

In [9]:
from natu.units import degC, degF, dB

These units are called lambda units because they involve invertible functions that are not limited to multiplication and division. For convenience, however, lambda units are overloaded to use the multiplication and division operators (* and /). The unit must always be on the right side of the operator:

In [10]:
temperature = 25*degC

We can display this temperature in kelvin (a scalar unit):

In [11]:
from natu.units import K
temperature/K
Out[11]:
298.1500

or in Fahrenheit (another lambda unit):

In [12]:
temperature/degF
Out[12]:
77.0000

We can add temperatures that have been created from different units:

In [13]:
0*degC + 100*K
Out[13]:
100.0 degC

The display unit of the first quantity takes precedence. natu checks that the dimensions are compatible when performing arithmetic, so a temperature can only be added to another temperature. Temperatures are absolute quantities, so the sum of 25 ℃ and 25 ℃ is 323.15 ℃ (or 596.3 K, but not 50 ℃):

In [14]:
25*degC + 25*degC
Out[14]:
323.15 degC

We can use the decibel to multiply two numbers by adding their logarithms:

In [15]:
(10/dB + 10/dB)*dB
Out[15]:
100.0000

Lambda units can be prefixed if they're set to allow it. In fact, the decibel is simply defined as the bel (B) with the deci (d) prefix. To retrieve a prefixed unit, import it as before:

In [16]:
from natu.units import mdegC
100*mdegC/K
Out[16]:
273.2500

Arrays and other data types

Quantities can be used in numpy arrays:

In [17]:
import numpy as np
np.array([1, 2, 3])*m
Out[17]:
array([1.0 m, 2.0 m, 3.0 m], dtype=object)

Notice that the result is an array of quantities. To get a quantity with a value that is an array, put the unit before the array:

In [18]:
m*np.array([1, 2, 3])
Out[18]:
[ 1.  2.  3.] m

The previous two lines of code are mathematically equivalent. However, when working with large arrays of quanties with the same physical dimension, the second form will generally save memory and processing time. It's also possible to create arrays of quantities with mixed dimensions, for example:

In [19]:
np.array([1*m, 100*degC])
Out[19]:
array([1.0 m, 100.0 degC], dtype=object)

Note that a quantity with a value that is an array can be indexed like an array. For example:

In [20]:
x = m*np.array([1, 2, 3])
x[:2]
Out[20]:
[ 1.  2.] m

Also, properties and methods of the array are available from the quantity. For example:

In [21]:
x.clip(0, 2)
Out[21]:
[ 1.  2.  2.] m

The value of a quantity can be complex:

In [22]:
(1 + 1j)*m
Out[22]:
(1+1j) m

Accessing groups of units

For convenience, the contents of natu.units are sorted into submodules of natu.groups. There are submodules for physical constants (constants), SI units (si), and units of various dimensions (length, time, pressure, etc.). We can access units from these submodules in several ways, just like those from natu.units:

In [23]:
# Individually:
from natu.groups.length import m

# By access:
from natu.groups import length
m = length.m

# By wildcard:
from natu.groups.length import *

Simplifying units

By default, units are simplified using coherent relations. For example,

In [24]:
from natu.units import m, kg, s
1*kg*m**2/s**2
Out[24]:
1.0 J

The extent of simplification can be adjusted using simplification_level in natu.config.

Defining new units

In code

We can derive a new coherent unit from existing units since the product or quotient of two units is generally another unit (as discussed here). For example, we can represent the cubic inch as a stand-alone unit:

In [25]:
cinch = inch**3
cinch
Out[25]:
ScalarUnit inch3 with dimension L3 (not prefixable)

However, notice that the display unit is not the new unit but rather inch3:

In [26]:
10*cinch
Out[26]:
10.0 inch3

We can update the display unit, but we must insert the new unit into the unit space to make it available:

In [27]:
cinch.display_unit = 'cinch'
from natu import units
units.cinch = cinch

Now we get the desired result:

In [28]:
10*cinch
Out[28]:
10.0 inch3

If the expression of the new unit requires a numeric factor (not coherently derived), then the result is a quantity that we need to explicitly cast as a unit:

In [29]:
from natu.core import ScalarUnit
from natu.units import ns
shake = ScalarUnit.from_quantity(10*ns, 'shake')

As before, we need to insert the unit into the unit space:

In [30]:
units.shake = shake

Now it's ready for use:

In [31]:
time = 500*ns
time.display_unit = 'shake'
time
Out[31]:
500.0 ns

Via definition files

We can also introduce new units by swapping or adding definition files. That way we don't need to re-enter the units during each Python session as with the methods in the previous section. Also, coherent units are automatically added to the list of coherent relations for use in simplifying units. However, it's important to be sure that the definition files are from a trusted source since natu uses eval() to process them.

The config submodule contains a list of definition files:

In [32]:
from natu import config
config.definitions
Out[32]:
['natu/config/base-SI.ini',
 'natu/config/derived.ini',
 'natu/config/BIPM.ini',
 'natu/config/other.ini']

These files are loaded and processed the first time any units are imported. Before that, we can change the list of files. For example:

In [33]:
config.definitions.append("custom.ini")

where custom.ini is in the current directory. Since units were imported before this section, it's necessary to restart the IPython notebook's kernel just before running this section so that the new file will be loaded and processed. Only then will the new units be available for import from natu.units or the appropriate submodules of natu.groups.

In [34]:
from natu.units import shake
shake
Out[34]:
ScalarUnit shake with dimension T (not prefixable)

Advanced topics

String formatting

By default, units and dimensions are formatted with "*" as the multiplication operator. Exponents directly follow the units and dimensions (without "**" or "^"). If necessary, a division sign is used (instead of negative exponents) and the denominator is placed in parentheses if it has more than one factor. Let's see how this looks for the Ampere constant (kA):

In [35]:
from natu.units import k_A
k_A.display_unit = 'N/A2'
print('{0} (dimension {0.dimension})'.format(k_A))
dyn/abA2 (dimension L*M/(I2*T2))

We can change the format using string format syntax. For example, the multiplication is shown by periods in Modelica format:

In [36]:
print('{0:M} (dimension {0.dimension:M})'.format(k_A))
dyn/abA2 (dimension L.M/(I2.T2))

In verbose format, the exponents are written as Python code:

In [37]:
print('{0:V} (dimension {0.dimension:V})'.format(k_A))
dyn / abA**2 (dimension L * M / (I**2 * T**2))

In Unicode format, the exponents are written as superscripts:

In [38]:
print(u'{0:U} (dimension {0.dimension:U})'.format(k_A))
dyn abA⁻² (dimension L M I⁻² T⁻²)

This format can only be used with integer exponents. In HTML format:

In [39]:
print('{0:H} (dimension {0.dimension:H})'.format(k_A))
dyn&nbsp;abA<sup>-2</sup> (dimension L&nbsp;M&nbsp;I<sup>-2</sup>&nbsp;T<sup>-2</sup>)

This renders as 1×10-7 N A-2 (dimension L M I-2 T-2).

Finally, in LaTeX math format:

In [40]:
print('{0:L} (dimension ${0.dimension:L}$)'.format(k_A))
\mathrm{dyn}\,\mathrm{abA}^{-2} (dimension $\mathrm{L}\,\mathrm{M}\,\mathrm{I}^{-2}\,\mathrm{T}^{-2}$)

This renders as $1 \times 10^{-7}\,\mathrm{N}\,\mathrm{A}^{-2}$ (dimension $\mathrm{L}\,\mathrm{M}\,\mathrm{I}^{-2}\,\mathrm{T}^{-2}$).

We can also change the format of the number using Python's built-in formatting. For example, to use Unicode format with seven decimal places:

In [41]:
print(u'{0:.7fU}'.format(k_A))
dyn abA⁻²

Changing the display unit of a unit

Each unit has display unit. It defaults to the unit itself, but it can be changed. We can use this feature to automatically convert units. For example, to enter a length in yards but display it in meters:

In [42]:
from natu.units import yd
yd.display_unit = 'm'
1*yd
Out[42]:
1.0 yd

Besides the display unit, a unit is essentially immutable. Its value is a protected property (_value) and its dimension and prefixable properties can't be changed.

Conversion factors

natu doesn't use conversion factors directly, but can divide units to determine them:

In [43]:
%precision 4
from natu.units import inch, m
inch/m
Out[43]:
0.0254

The result may seem counterintuitive at first. We divided the inch by the metre, but we got the conversion factor from inches to metres. The conversion factor from unit A to unit B is the number of units B in one unit A. Mathematically, this is x*B = 1*A, where x is the conversion factor. The solution is x = A/B. In this case A is the inch and B is the metre, so we have the conversion factor from inches to metres (which happens to be the number we used in the first section).

In natu we deal with quantities, not numbers. Numbers are unit-dependent, but quantities are not. A quantity is expressed as the product of a number and a unit (q = n*U). When we say "in unit," we generally mean "divided by unit." So "quantity in unit" is q/U or n, the number.

Disabling quantities

Mathematical operations on natu's quantities are slower than those on floats because the quantities track dimension and display unit in addition to the value. However, quantities may only be needed to check dimensions during code development and to display values as the product of a number and a unit for debugging. Later, one can speed up the code by disabling quantities and using their values directly.

First, let's see how long it takes to express a simple quantity:

In [44]:
%timeit 1*m
10000 loops, best of 3: 25.9 µs per loop

We'll disable quantities and check again. Restart the IPython kernel now, since it's only possible to disable quantities before any units are imported. Then,

In [1]:
%precision 4
from natu import config
config.use_quantities = False

Now, import the desired units:

In [2]:
from natu.units import m
%timeit 1*m
10000000 loops, best of 3: 54.4 ns per loop

The operation is about 300 times faster because it's just floating point multiplication. The metre is now a float:

In [3]:
type(m)
Out[3]:
float

However, the core functionality is still available. As in the first section of the tutorial, where quantities were enabled, we can express a length as a number of inches by dividing it by the inch:

In [4]:
from natu.units import inch
length = 0.0254*m
length/inch
Out[4]:
1.0000

If we inspect the length directly, we find that it's only a number:

In [5]:
length
Out[5]:
0.0254

This is the number of metres, since the metre has a value of 1.0:

In [6]:
m
Out[6]:
1.0000

The metre is normalized because the units were based on SI using base-SI.ini (the first definition file, by default). This can be changed (see the documentation and the earlier section on units via definition files), and code generally shouldn't be written so that it requires a particular unit system.

Although scalar units are now floats, lambda units remain. Let's look at the degree Celsius:

In [7]:
from natu.units import degC
degC
Out[7]:
dimensionless LambdaUnit degC (prefixable)

It's considered dimensionless because dimensions are no longer tracked; it yields a float instead of a quantity. The usage is the same though:

In [8]:
from natu.units import K
temperature = 0*degC + 100*K
temperature/degC
Out[8]:
100.0000