This tutorial shows how to use the natu module to work with physical quantities in Python.
Table of contents:
Before we get started, we'll set the display precision for this IPython notebook.
%precision 4
u'%.4f'
First, we'll import the metre (m):
from natu.units import m
m
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:
length = 0.0254*m
length
0.0254 m
We can change the display unit by setting the quantity's display
property:
length.display_unit = 'inch'
length
0.0254 m
We can also express the length as a number of inches by dividing it by the inch:
from natu.units import inch
length/inch
1.0000
from natu.units import km
km
ScalarUnit km with dimension L (not prefixable)
It can't be prefixed any further. As expected, it's 1000 times larger than the metre:
km/m
1000.0000
We can access units directly from the units module, with or without prefixes:
from natu import units as U
U.km/U.m
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 such as the degree Celsius, degree Fahrenheit, and decibel are available:
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:
temperature = 25*degC
We can display this temperature in kelvin (a scalar unit):
from natu.units import K
temperature/K
298.1500
or in Fahrenheit (another lambda unit):
temperature/degF
77.0000
We can add temperatures that have been created from different units:
0*degC + 100*K
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 ℃):
25*degC + 25*degC
323.15 degC
We can use the decibel to multiply two numbers by adding their logarithms:
(10/dB + 10/dB)*dB
100.0000
from natu.units import mdegC
100*mdegC/K
273.2500
Quantities can be used in numpy arrays:
import numpy as np
np.array([1, 2, 3])*m
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:
m*np.array([1, 2, 3])
[ 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:
np.array([1*m, 100*degC])
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:
x = m*np.array([1, 2, 3])
x[:2]
[ 1. 2.] m
Also, properties and methods of the array are available from the quantity. For example:
x.clip(0, 2)
[ 1. 2. 2.] m
The value of a quantity can be complex:
(1 + 1j)*m
(1+1j) m
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:
# Individually:
from natu.groups.length import m
# By access:
from natu.groups import length
m = length.m
# By wildcard:
from natu.groups.length import *
By default, units are simplified using coherent relations. For example,
from natu.units import m, kg, s
1*kg*m**2/s**2
1.0 J
The extent of simplification can be adjusted using simplification_level
in natu.config.
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:
cinch = inch**3
cinch
ScalarUnit inch3 with dimension L3 (not prefixable)
However, notice that the display unit is not the new unit but rather inch3:
10*cinch
10.0 inch3
We can update the display unit, but we must insert the new unit into the unit space to make it available:
cinch.display_unit = 'cinch'
from natu import units
units.cinch = cinch
Now we get the desired result:
10*cinch
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:
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:
units.shake = shake
Now it's ready for use:
time = 500*ns
time.display_unit = 'shake'
time
500.0 ns
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:
from natu import config
config.definitions
['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:
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.
from natu.units import shake
shake
ScalarUnit shake with dimension T (not prefixable)
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):
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:
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:
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:
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:
print('{0:H} (dimension {0.dimension:H})'.format(k_A))
dyn abA<sup>-2</sup> (dimension L M I<sup>-2</sup> 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:
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:
print(u'{0:.7fU}'.format(k_A))
dyn abA⁻²
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:
from natu.units import yd
yd.display_unit = 'm'
1*yd
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.
natu doesn't use conversion factors directly, but can divide units to determine them:
%precision 4
from natu.units import inch, m
inch/m
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.
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:
%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,
%precision 4
from natu import config
config.use_quantities = False
Now, import the desired units:
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:
type(m)
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:
from natu.units import inch
length = 0.0254*m
length/inch
1.0000
If we inspect the length directly, we find that it's only a number:
length
0.0254
m
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:
from natu.units import degC
degC
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:
from natu.units import K
temperature = 0*degC + 100*K
temperature/degC
100.0000