Unum
Tutorial
by Pierre X. Denis
Spacebel, Belgium
last updated - March 24, 2004
____________________________________________
Table of
Contents
____________________________________________
Integration with mathematical functions
Integration with Python's types
Alternative Ways To Define Units
Integration with Numerical Python
Note About Older Unum versions
____________________________________________
Unum stands for 'unit-numbers'. It is a Python module that allows to
define and manipulate true quantities, i.e. numbers with units such as
60 seconds, 500 watts, 42 miles-per-hour, 100 kg per square meter, 14400 bits
per second, 30 dollars etc. The module validates unit consistency in arithmetic
expressions; it provides also automatic conversion and output formatting. Unum
is designed to be reliable, easy-to-use, customizable and open to any unit
definition. But this is maybe a subjective point-of-view. So… try it and make
your mind !
Before going any further, let me clarify the 'unumology' (the science
of unum terminology) :
Unum = the main class
defining quantities
unum = a quantity,
i.e. an instance of Unum
enum = C slang;
nothing to do here
The present tutorial basically explains how to install Unum and how to
use it as an interactive calculator. Then, it explains how to define new units.
Finally, for advanced use, it gives clues on Unum customization such as output
formatting. The tutorial assumes a basic knowledge of the Python language.
Should you notice error or discrepancy with the actual Unum behavior,
please report it to Pierre.Denis@spacebel.be.
In order to run the examples given in the present tutorial, you need :
Ø a computer
running Python 2.2 or later version,
Ø Unum ver 4.0
installation file (download at http://home.tiscali.be/be052320/download.html),
in the format that is best suited for your platform (see below).
·
For Windows,
® download and
execute the win32.exe installer.
·
For Red Hat Linux,
® download the RPM
file and install it with the adequate tool.
·
For other OS,
® download the zip
file and follow the following steps to install Unum :
1.
unzip Unum installation files in <your-install-directory>
2.
In your terminal, type
cd <your-install-directory>/Unum-4.0
python
setup.py install
3.
if the installation is successful, you can safely remove <your-install-directory>
This will install Unum packages in your Python site-packages directory,
i.e. it will create the directory
<python-site-packages-dir>/unum
with different subdirectories in it.
To check that the installation is successful, you could run the test
cases and see that no error occurred. In your terminal, type
python
<python-site-packages-dir>/unum/tools/test.py
A couple of lines should report the results of the test cases, which
are expected to be all successful.
Note : depending on your platform, some spurious errors may be reported due
to numerical precision issues.
There are basically two ways you can use Unum to make interactive
calculations.
>>>
import unum.units
>>>
All
the examples of the present tutorial still apply in this environment but there
are two drawbacks for an interactive session : 1° in case of unit
inconsistencies, unfriendly exception trace messages are displayed, 2° some
facility functions are not available (see Unit Catalog Functions). Actually, the
use of this import statement is advised to embed unums in applications (see A Small
Application).
Once you get the prompt, you may try the following as a test :
>>>
M
1.0 [m]
>>>
The result '1.0 [m]' means that the unum named M, which
represents one meter, is defined in the session's namespace. A unum is
represented as a number, followed its unit between brackets. M is actually a
variable defined in the unum.units package
(imported implicitly by the Unum calculator). Other base units are available
like S for second, KG for kilogram, etc. To get
the full list, please refer to Unit
Catalog Functions. Note that any unit can be (re-) defined
according to the specific needs of your application domain (see section How
To Define New Units).
Any quantity can be defined by multiplying a predefined unit by a
number. The examples in this section assume that the unum variables M, KG, S have been defined (this is
the case if you are running the Unum calculator or if you imported unum.units).
>>>
50 * M
50.0
[m]
>>>
25 * S
25.0
[s]
>>>
Note that integers are automatically casted to floating point number,
mainly in order to avoid the problem of integer division. Units may be combined
by multiplication, division or exponentiation to produce any quantity:
>>>
3 * M/S
3.0
[m/s]
>>>
1 / (3 * M/S)
0.333333333333
[s/m]
>>>
25 * M**2
25.0
[m2]
>>>
(3 * M) * (4 * M)
12.0
[m2]
>>>
10 * KG*M/S**2
10.0
[kg.m/s2]
>>>
The priority of operator evaluation is inherited from Python's, so you
have to be careful and use brackets if needed. You may notice that if a unit is
multiplied by itself then the exponent is indicated just after the unit (e.g. [m2]); if different
units are multiplied together then they are separated by a dot. All the units
after the slash (/) represent units present in the denominator,
i.e. they have a negative exponent. After evaluation, if a unit's exponent
boils down to 0 (same exponent on numerator and denominator), then it is no
longer displayed. As a special case, a unum expression may be evaluated as a
unit-less quantity; it is conceptually equivalent to a raw number, although
represented by an empty unit group [] :
>>>
M / M
1.0
[]
>>>
(2 * M/S) * (3 * S/M)
6.0
[]
>>>
These unit-less unums may be easily converted to true numbers (see Integration with mathematical functions).
Note that unit output formatting is customizable (see Unum Customization); for instance the
empty unit group[] may be removed by setting the parameter UNIT_HIDE_EMPTY to True.
All the expressions seen so far produce unums and the units used to
define them are also unums. These unums may be assigned to variables and
manipulated as such, following the Python syntax :
>>>
distance = 50 * M
>>>
distance
50.0
[m]
>>>
volume = distance ** 3
>>>
volume
125000.0
[m3]
>>>
duration = 25 * S
>>>
duration
25.0
[s]
>>>
speed = distance / duration
>>>
speed
2.0
[m/s]
>>>
mass = 1.5 * KG
>>>
kinetic_energy = (mass * speed**2) / 2
>>>
kinetic_energy
3.0
[kg.m2/s2]
>>>
So far we just used the multiplication, division or exponentiation
operators; obviously unums may be added to or subtracted from each other :
>>>
distance + 20*M
70.0
[m]
>>>
distance**2 + 20 * M**2
2520.0
[m2]
>>>
speed - 6 * M/S
-4.0
[m/s]
>>>
The augmented assignments can be used to modify the value associated to
a variable.
>>>
distance += 100 * M
>>>
distance
150.0
[m]
>>>
speed *= 2
>>>
speed
4.0
[m/s]
>>>
Note that the right part of *= and /= should be a unit-less number,
in order to keep the dimensional consistency of variable's meaning; however
this is not checked automatically. Unums may also be compared together through
the standard comparison operators :
>>>
distance >= speed * 20 * S
True
>>>
distance**2 == 20 * M**2
False
>>>
The addition, subtraction and comparison operators may fail if the
units are not compatible; if this occurs, then an error message is displayed
explaining the cause of incompatibility :
>>>
distance = 50 * M
>>>
distance + 3 * KG
unum.DimensionError: [m] incompatible with [kg]
>>>
distance**2 - 20*M
unum.DimensionError: [m2] incompatible with [m]
>>>
distance + 125
unum.DimensionError: [m] incompatible with []
>>>
distance + speed
unum.DimensionError: [m] incompatible with [m/s]
>>>
distance < speed
unum.DimensionError: [m] incompatible with [m/s]
>>>
speed += 20
unum.DimensionError: [m/s] incompatible with []
>>>
duration == 15 * KG
unum.DimensionError: [s] incompatible with [kg]
>>>
Another kind of check is related to exponentiation : the exponent must
be a number without units. Meanwhile, the exponent may be an expression
containing unums, provided that the units vanish after evaluation.
>>>
M ** KG
unum.DimensionError: unit [kg] unexpected
>>>
M ** (M/S)
unum.DimensionError: unit [m/s] unexpected
>>>
M ** (duration/S)
1.0
[m25.0]
>>>
2 ** (duration /S)
33554432.0
[]
>>>
Note that these last examples work only if the AUTO_NORM parameter is
left to its default value, i.e. True (see Unum Customization).
These systematic consistency checks are probably the greatest benefit
of using Unum. It forces you to be consistent and it notifies you about any
incompatibility.
Several units may characterize the same dimension. For example, meters,
kilometers, miles, inches relate to the
same dimension : a length. Other dimensions include duration, mass, speed,
surface, volume, etc… Each unit may be converted to another unit of the same
dimension, thanks to a specific factor. Therefore, it makes sense to add or
subtract unums with different units, provided that they relate to the same
dimension; the conversion factors are automatically applied:
>>>
TON + 500*KG
1.5
[t]
>>>
5E-8*M - 28*ANGSTROM
472.0
[angstrom]
>>>
3*H + 20*MIN + 15*S
3.3375
[h]
>>>
H == 60*MIN
True
>>>
10000*S > 3*H + 15*MIN
False
>>>
Conceptually, the resulting unit may be chosen arbitrarily between the
operand's units, or even from any other units related to the same dimension.
Here, the choice is made by Unum; it actually depends on how the base units
have been defined (see section later). If you want the result in a specific
unit, then you have to use the as method. The syntax is the
following :
(unum expression).as(target unit)
The brackets around the unum expression are optional if the method is
applied to a variable. Here are some examples :
>>>
M.as(ANGSTROM)
10000000000.0
[angstrom]
>>>
ANGSTROM.as(M)
1e-010
[m]
>>>
(3*H + 20*MIN + 15*S).as(S)
12015.0
[s]
>>>
(3*H + 20*MIN + 15*S).as(MIN)
200.25
[min]
>>>
from math import pi
>>>
(2*pi*RAD).as(ARCDEG)
360.0
[deg]
>>>
energy = 3 * KG*(M/S)**2
>>>
energy
3.0
[kg.m2/s2]
>>>
energy.as(J)
3.0
[J]
>>>
energy.as(N*M)
3.0
[N.m]
>>>
energy.as(W*S)
3.0
[W.s]
>>>
energy.as(W*H)
0.000833333333333
[W.h]
>>>
The as method has no permanent effect on the
variable on which it is applied; if you want a permanent conversion, then the
assignment must be used as in the following example :
>>>
energy
3.0
[kg.m2/s2]
>>>
energy = energy.as(W*H)
>>>
energy
0.000833333333333
[W.h]
>>>
As you can expect, unit compatibility is checked by the as method, since
the unum on the left must have the same dimension as the unum on the right :
>>>
M.as(KG)
unum.DimensionError: [m] incompatible with [kg]
>>>
energy.as(W)
unum.DimensionError: [W.h] incompatible with [W]
>>>
By the way, this is a very useful way to check that a given quantity
has got the expected dimension.
In order to use mathematical functions like sin, log and floor
on unums, the argument must be unit-free, as demonstrated in the example below
:
>>>
from math import log10, cos, sin
>>>
log10(M/ANGSTROM)
10.0
>>>
cos(180*ARCDEG)
-1.0
>>>
cos(pi*RAD)
-1.0
>>>
f = 440*HZ
>>>
sin(f)
unum.DimensionError: unit [Hz] unexpected
>>>
dt = 0.1 * S
>>>
sin(f*dt*2*pi)
-3.9198245344040927e-014
>>>
Note that this works only if the AUTO_NORM parameter is
left to its default value, i.e. True (see Unum Customization). In other circumstances,
if raw numbers must be extracted explicitly from unums, the functions complex, int, long or float may be used; the
argument must, again, be unit-free :
>>>
long(M)
unum.DimensionError: unit [m] unexpected
>>>
long(M/ANGSTROM)
10000000000L
>>>
complex(f*dt*2*pi)
(276.46015351590177+0j)
>>>
As an alternative, there is also the asNumber method : if
called without argument, it simply extracts the raw number from the unum, without
any check; if called with one argument, then a dimension-compatible unit is
expected; the unum is converted towards this unit before getting the raw
number.
>>>
(20*M).asNumber()
20.0
>>>
(20*M).asNumber(M)
20.0
>>>
(20*M/S).asNumber(M)
unum.DimensionError: [m/s] incompatible with [m]
>>>
(20*M/S).asNumber(M/MIN)
1200.0
>>>
Thanks to Python's dynamic typing, unums may be combined with any
built-in types. Here are some examples of integration with complex numbers and
lists.
>>>
length = 1j * M
>>>
length
1j
[m]
>>>
length**2
(-1+0j)
[m2]
>>>
distances = [10*MILE, 18500*M, 5E-9*UA, 3E13*ANGSTROM]
>>>
distances
[10.0
[mile], 18500.0 [m], 5e-009 [ua], 3e+013 [angstrom]]
>>>
distances.sort()
>>>
distances
[5e-009
[ua], 3e+013 [angstrom], 18500.0 [m], 10.0 [mile]]
>>>
[d.as(M) for d in distances]
[747.99
[m], 3000.0 [m], 18500.0 [m], 18520.0 [m]]
>>>
Of course, new types like matrixes and distributions of unums should be
easily defined (see also Integration with Numerical Python).
Unum is provided with a library of predefined Unum. Since version 4.0,
it includes all the SI units as defined by NIST (http://physics.nist.gov/cuu/Units/units.html), as well as
some other widely used units (http://physics.nist.gov/cuu/Units/outside.html). This library
is structured into hierarchical modules that can be imported in different ways,
according to user needs. The following table shows the different modules and
the units they contain.
unum.units.si.base |
the 7 SI base units |
unum.units.si.derived |
the 7 SI base units + the derived SI units |
unum.units.si |
the 7 SI base units + the derived SI units (equivalent to previous) |
unum.units.others |
the 7 SI base units + the derived SI units + other widely used units |
unum.units.custom |
user-defined units (empty by default) |
unum.units |
the 7 SI base units + the derived SI units + other widely used unit |
The unum calculator uses the most general module unum.units , which imports all
the units defined. The precise set of imported units may be displayed by
calling the ucat function (see Unit Catalog Functions).
Two strings are involved in each unit : the symbol (e.g '[m]')
which is used to display unum and the name (e.g. 'meter') which is just
informative. Furthermore, each unit is referred by a variable name (e.g M) that is used in
expressions. The symbol and name have been written respectfully to the
definition. However, in order to minimize the risk of name collision, the
choice have been made to spell variable name in uppercase, in regard with the
symbol (symbol [m] associated to variable M). This
convention is inspired from the C language where constants are usually defined
as uppercase. This has led to a couple of 'irregular' names :
Ø BEL : instead of B, which is already used for
Barn,
Ø SIEMENS : instead of S, which is
already used for second,
Ø TON : instead of T, which is
already used for Tesla,
Ø and the
suppression of RAD as centi-gray (0.01
Gy) which is used for the radian (angle).
Independently, other names/symbols have been adapted because of issues
in displaying non-standard characters:
variable name |
symbol |
name |
ANGSTROM |
[angstrom] |
angstrom, official symbol [Å] |
ARCDEG |
[deg] |
degree (angle unit), official symbol [°] |
ARCMIN |
['] |
minute (angle unit) |
ARCSEC |
[''] |
second (angle unit) |
CELSIUS |
[deg C] |
degree Celsius (temperature unit), official symbol [°C] |
OHM |
[ohm] |
ohm, official symbol [W] |
To end this section, here are three important cautions :
Ø Be very careful
of name collisions : there is a real
risk that you reassign a unit variable name, which is inherent to Python's
variable binding. The risk is fully eliminated if you use only lowercase
variables, or at least one character in lowercase. To limit further the risk of
name collision, the import … as … statement may be used to
identify unit variables by a given prefix; for instance
>>>
import unum.units.si as SI
>>>
20 * SI.M / SI.S
20.0
[m/s]
>>>
Ø The name
conflicts between time, temperature and angle units have been solved as
follows:
variable name |
symbol |
name |
S |
[s] |
second (time unit) |
ARCSEC |
[''] |
second (angle unit) |
MIN |
[min] |
minute (time unit) |
ARCMIN |
['] |
minute (angle unit) |
CELSIUS |
[deg C] |
degree Celsius (temperature unit) |
ARCDEG |
[deg] |
degree (angle unit) |
Ø Unum is unable to
handle reliably conversions between °Celsius and Kelvin. The issue is referred as
the 'false origin problem' : the 0°Celsius is defined as 273.15 K. This is
really a special and annoying case, since in general the value 0 is unaffected
by unit conversion, e.g. 0 [m] = 0 [miles] = ... . Here, the conversion
Kelvin/°Celsius is characterized by a factor 1 and an offset of 273.15 K. The
offset is not feasible in the current version of Unum.
Moreover it will presumably never be integrated in a future version because
there is also a conceptual problem : the offset should be applied if the quantity
represents an absolute temperature, but it shouldn't if the quantity represents
a difference of temperatures. For instance, a raise of temperature of 1°
Celsius is equivalent to a raise of 1 K. It is impossible to guess what is in
the user mind, whether it's an absolute or a relative temperature. The question
of absolute vs relative quantities is unimportant for other units since the
answer does not impact the conversion rule. Unum is unable to make the
distinction between the two cases.
In the unit packages, the Kelvin and degree Celsius are defined under the names
K and CELSIUS. For
conversions, relative temperatures are always assumed, so there is no
offset added. For instance,
>>>
K.as(CELSIUS)
1.0
[deg C]
>>>
CELSIUS.as(K)
1.0
[K]
>>>
It may be useful to display the list of all available units. This is
the purpose of the udict and ucat functions
available in any Unum calculator session (these functions may also be imported
from the unum.calc module).
The udict function returns a
dictionary with all the defined unums (imported units and user-defined
quantities), indexed by their names. It is actually a subset of the standard globals() Python
function's result :
>>>
udict()
{'HZ':
1.0 [Hz], 'BAR': 1.0 [bar], 'WB': 1.0 [Wb],… }
>>>
The ucat function can be called to display all the
available units, sorted alphabetically, with their symbol, conversion
expression (if any) and name :
>>>
ucat()
A : [A] : ampere
ANGSTROM : [angstrom] = 1e-010 [m] : angstrom
ARE : [a] = 100.0 [m2]
: are
B : [b] = 1e-028 [m2]
: barn
BAR : [bar] = 100000.0 [Pa]
: bar
…
>>>
The ucat function can
also be called for one specific unit :
>>>
ucat(N)
N : [N] = 1.0 [kg.m/s2]
: newton
>>>
ucat(J)
J
: [J] = 1.0 [N.m] : joule
>>>
For advanced use, one may call the getUnitTable static method on the Unum class:
>>>
from unum import Unum
>>>
Unum.getUnitTable()
{'ohm':
(1.0 [V/A], 5, 'ohm'), 'rad': (1.0 [], 1, 'radian'), …
>>>
It returns a copy of the Unum's internal data structure, namely a
dictionary having unit symbol as key and a 3-tuple giving 1° the equivalent converted
unum (or None for basic units), 2° the unit level and 3°
the unit name. The unit level is recursively defined as
0, for
the basic units,
1 + the highest level among the
units appearing in the converted expression, otherwise.
So far we have seen Unum in action in an interactive calculator. Unum
can be used in any Python application that require unit consistency checking.
Here is a small application that calculates the gravitation force and
accelerations of two bodies, for given lists of distances and masses.
# -- Unit definitions
from unum.units import *
from unum import Unum
KM =
Unum.unit('km' , 1000. * M )
CM =
Unum.unit('cm' , .01 * M )
GRAM = Unum.unit('g' ,
.001 * KG)
# -- Constants
G
= 6.6720E-11 * N*M**2/KG**2
earth_mass
= 5.980E24 * KG
c
= 299792458 * M/S
earth_radius = 6.37E+06 * M
# -- Input Data
distances = (5*CM, earth_radius, c *
365*24*H)
masses
= (5*GRAM, earth_mass, 1000*earth_mass)
# -- Processing and display
print "G = %s" % G
print "Earth mass = %s" % earth_mass
print "Earth radius = %s" %
earth_radius.as(KM)
print "distances = %s" % str(distances)
print "masses = %s" % str(masses)
print
for m1 in masses:
for
m2 in masses:
if m1 >= m2:
for d in distances:
force = G*m1*m2/d**2
a1 = force/m1
a2 = force/m2
print "m1 = %s, m2 = %s, d = %s" % (m1,
m2, d)
print "f = %s, a1 = %s, a2 = %s\n"
% (force.as(N), a1.as(M/S**2), a2.as(M/S**2))
Note that KM, CM and GRAM units have been created
on-the-fly because they are not defined in unum.units module. The
output of this application is sampled hereafter :
G
= 6.672e-011 [N.m2/kg2]
Earth mass
= 5.98e+024 [kg]
Earth radius = 6370.0 [km]
distances
= (5.0 [cm], 6370000.0 [m], 9.45425495549e+015 [m])
masses
= (5.0 [g], 5.98e+024 [kg], 5.98e+027 [kg])
m1 = 5.0 [g], m2 = 5.0 [g], d = 5.0 [cm]
f = 6.672e-013 [N], a1 = 1.3344e-010 [m/s2],
a2 = 1.3344e-010 [m/s2]
m1 = 5.0 [g], m2 = 5.0 [g], d = 6370000.0 [m]
f = 4.11071323832e-029 [N], a1 =
8.22142647664e-027 [m/s2], a2 = 8.22142647664e-027 [m/s2]
…
The examples given so far are based on the units defined in unum.units. This module
contains the SI units and some units outside SI, although widely used.
According to the domain of application (engineering, physics, chemistry,
finance, …), you could have to define your own subsets of units with the
related conversion rules. To achieve this, the simpler way of doing is adding
your own units in the following file :
<python-site-packages-dir>/unum/units/custom/__init__.py
You have simply to follow the examples commented in this file and to
take care of name collisions (note that the examples given below may also be
embedded in this __init__.py file). Then, these custom units will be automatically available
at the next Unum calculator session or if you import unum.units. Of course, you
could also define a new unit module with your own name and import it instead of
unum.units.
New units could also be created 'on-the-fly' inside a Unum session; the
following examples show how to proceed. It uses the function unit directly available
in a Unum calculator session. If you do not use the calculator, you have to
know that unit is a static method of Unum class; here is a common
idiom to make this method visible (respect the case !) :
>>>
from unum import Unum
>>>
unit = Unum.unit
>>>
Imagine now you want to define a new unit called 'spam', with the three
derived units 'kilospam', 'millispam' and 'sps', i.e. spam per second (in the
following, we assume that 'second' unit is referred as S). The base unit
must be defined first :
>>>
SPAM = unit('spam')
>>>
This statement means that the variable SPAM now refers to a
unum representing a quantity of one 'spam'. From here, derived units may be
defined :
>>>
KSPAM = unit('kilospam' , 1000.0 * SPAM)
>>>
MSPAM = unit('millispam' , 0.001 *
SPAM)
>>>
SPS = unit('sps' , SPAM / S)
>>>
The second argument is a Unum expression giving the unit being defined
in terms of other unit(s). Note that the name of the variable (capitalized) is
arbitrary and independent of the unit symbol (string between quotes); for
example KSPAM is used to
designate one 'kilospam'. Now you are able to work with 'spammed' quantities :
>>>
20 * SPAM
20.0
[spam]
>>>
500 * MSPAM
500.0
[millispam]
>>>
(500 * MSPAM).as(SPAM)
0.5
[spam]
>>>
2 * KSPAM + 3 * SPAM + 4 * MSPAM
2.003004
[kilospam]
>>>
3 * SPAM + 20 * S
unum.DimensionError: [spam] incompatible with [s]
>>>
5 * SPS * 10 * S
50.0
[spam]
>>>
(50 * KSPAM / S).as(SPS)
50000.0
[sps]
>>>
(SPS).as(MSPAM/S)
1000.0
[millispam/s]
>>>
Note : in some cases, it is necessary to use special units expressed as
a standard unit multiplied by a power of 10. Here is a way to handle this.
>>>
DIST = unit('1e-25 M',1e-25*M)
>>>
20 * DIST
20.0
[1e-25 M]
>>>
M.as(DIST)
1e+025
[1e-25 M]
>>>
Considering the previous section, there are actually two alternatives
to define derived units, which entail different behaviors. These are connected
to different philosophical points of view about units and quantities. They may be
considered in specific circumstances, which are detailed below.
Note : for the following examples, it is safer to restart a new Unum
calculator session in order to erase the previous unit definitions. Another way
of doing is to type the following statement :
>>>
from unum import Unum
>>>
Unum.reset()
>>>
This removes all the conversion rules between units, without erasing
these units.
1. automatic conversion to base unit
The following statements are consistent with the definitions of 'spam'
derived units :
>>>
SPAM = unit('spam')
>>>
KSPAM = 1000.0 * SPAM
>>>
MSPAM = 0.001 * SPAM
>>>
SPS = SPAM / S
>>>
The difference from former definitions is that derived units don't
exist as such any longer, since they are converted directly toward the 'spam' base
unit. The conversion and compatibility between all the units derived from
'spam' is established de facto (this simple approach is used in Unum ver
1). The main drawback of this method is the unfriendly output formatting, since
you can not avoid the conversion of the data you enter :
>>>
25 * KSPAM
25000.0
[spam]
>>>
Another drawback is the potential of calculation inaccuracy (for very
small numbers) and the increased risk of under- / overflows since conversion coefficients
are applied even if they are unnecessary. For example the addition of two
quantities expressed as 'millispam' will suffer a division by 1000, which is
conceptually unwanted considering that the result could be expressed also in
'millispam'.
Note that the as method is meaningless here,
as shown below :
>>>
(125 * SPAM).as(KSPAM)
unum.DimensionError: 1000.0 [spam] not a basic unit
>>>
The error message indicates that the method requires a 'basic unit' as
argument, i.e. a unum with a coefficient equal to 1.
2. No automatic conversion
It is also feasible to drop the conversion expression in the definition
of the derived units :
>>>
SPAM = Unum.unit('spam')
>>>
KSPAM = Unum.unit('kilospam')
>>>
MSPAM = Unum.unit('millispam')
>>>
SPS = Unum.unit('sps')
>>>
In this case, each variable is considered to be a unit as such, with no
links with other unit(s). So, they cannot be added, subtracted or compared
together. And, obviously, the as method is inapplicable.
>>>
20 * KSPAM + 15 * SPAM
unum.DimensionError: [kilospam] incompatible with [spam]
>>>
(20*KSPAM).as(SPAM)
unum.DimensionError: [kilospam] incompatible with [spam]
>>>
These operations requires an explicit conversion factor, for example :
>>>
KSPAM2SPAM = 1000.0 * SPAM / KSPAM
>>>
SPAM2KSPAM = 1 / KSPAM2SPAM
>>>
20 * KSPAM * KSPAM2SPAM + 15 * SPAM
20015.0
[spam]
>>>
20 * KSPAM + 15 * SPAM * SPAM2KSPAM
20.015
[kilospam]
>>>
This way of doing is more cumbersome, but it may be attractive for
people who are suspicious about automatic conversion and who prefer a more
conservative approach. This is especially advisable if unums are used at
school, to learn children when and how they have to use conversion factors. It
is probably dangerous for a student to be trained to automatic conversion,
meanwhile the current standard calculators are not unit-aware.
Unums may be saved into files or databases thanks to the pickle and shelve modules. Here is
an example using pickle that saves a
tuple of 3 unums into the file 'test.u' :
>>>
from unum.units import *
>>>
import pickle
>>>
f = open('test.u','w')
>>>
pickle.dump((10*M,20*W,30*J),f)
>>>
f.close()
>>>
And here is the code to retrieve the data from 'test.u' in another
session :
>>>
from unum.units import *
>>>
import pickle
>>>
f = open('test.u','r')
>>>
length, power,energy = pickle.load(f)
>>>
f.close()
>>>
length,power,energy
(10.0
[m], 20.0 [W], 30.0 [J])
>>>
Note that the table that keeps all the unit's raw data (e.g. the
imported unum.units) is not saved; hence, for
each new session, it has to be imported before loading the file. If you use the
Unum calculator, then unum.units is automatically
imported, otherwise the import must be explicit, as shown in the example above.
Of course, provided that the session is not left, the import statements are
not required.
Numerical Python (NumPy) is a package that allows the definition of
compact multidimensional arrays, on which mathematical operations are processed
fast through high-level expressions. It may be downloaded at http://www.pfdubois.com/numpy.
Unum integrates very naturally and easily with it. Here are some examples and
remarks.
>>>
from Numeric import *
>>>
from unum.units import *
>>>
lengths = array([6, 8, 0, 5]) * M
>>>
lengths
[
6. 8.
0. 5.] [m]
>>>
In this first example, lengths is a unum with
an NumPy array as raw value. Note that, as the raw value of M is defined as the
floating point 1.0, the multiplication of the integers of the array by this
constant performs a casting to an array of floating point numbers. Note also
that the display of the array does not begin with 'array(' as it is the
case in NumPy; actually it is the same as if a 'print' statement was issued
(this is because, contrarily to NumPy, Unum does not make a distinction between
__repr__ and __str__ methods).
Since lengths is a unum, any operation on
it will first be ruled by Unum semantics (consistency checking, unit
conversion, etc), then this operation will be propagated to the array elements
following NumPy semantics.
>>>
2 * lengths
[
12. 16. 0. 10.] [m]
>>>
lengths ** 2
[
36. 64. 0. 25.] [m2]
>>>
For the remaining, we define the unit CM as one hundredth
of meter :
>>>
from unum import Unum
>>>
CM = Unum.unit('cm', .01 * M)
>>>
The examples below demonstrate the automatic unit consistency features
for arrays.
>>>
lengths + array([1]*4) * CM
[
601. 801. 1. 501.] [cm]
>>>
lengths + 1*CM
[
601. 801. 1. 501.] [cm]
>>>
(lengths + 1*CM).as(M)
[
6.01 8.01 0.01 5.01] [m]
>>>
lengths * 2*CM
[
0.12 0.16 0. 0.1 ] [m2]
>>>
speed = 20 * M/S
>>>
lengths / speed
[
0.3 0.4 0. 0.25] [s]
>>>
speeds = array([22,15,24,2]) * M/S
>>>
lengths / speeds
[
0.27272727 0.53333333 0.
2.5 ] [s]
>>>
lengths + speeds
unum.DimensionError: [m] incompatible with [m/s]
>>>
The array slicing works as expected :
>>>
lengths[1]
8.0
[m]
>>>
lengths[1:-1]
[
8. 0.] [m]
>>>
lengths[2] = 20
unum.DimensionError: [] incompatible with [m]
>>>
lengths[2] = 20 * CM
>>>
lengths
[
6. 8. 0.2 5. ] [m]
>>>
Note in this example that dimension consistency checking and unit
conversion are automatically performed for slice assignment.
For different technical reasons, most of NumPy's 'universal functions'
do not work directly on unums, even if they are unit-free (the exceptions, as
seen in the examples before, are the usual arithmetic operators +, -, *, /, **). We need here a special Unum
function, asNumber, that will extract the
array. This function must be called with an argument that gives a
dimension-compatible unit.
>>>
lengths.asNumber(M)
array([
6. , 8. , 0.2, 5. ])
>>>
lengths.asNumber(CM)
array([
600., 800., 20., 500.])
>>>
cos(lengths.asNumber(M))
array([
0.96017029, -0.14550003,
0.98006658, 0.28366219])
>>>
lengths.asNumber(M) < array([5,10,8,5])
array([0,
1, 1, 0])
>>>
In these examples, the returned objects are regular NumPy arrays, not
unums.
There is an other way to integrate NumPy arrays with Unum : it is by
defining an array with each unums as elements (note : the examples below
require NumPy 23.0 at least; a bug prevents the creation of array containing
unums in previous versions).
>>>
lengths2 = array([6*M, 8*M, 0*M, 5*M])
>>>
lengths2
array([6.0
[m] , 8.0 [m] , 0.0 [m] , 5.0 [m] ],'O')
>>>
2 * lengths2
array([12.0
[m] , 16.0 [m] , 0.0 [m] , 10.0 [m] ],'O')
>>>
lengths2 ** 2
array([36.0
[m2] , 64.0 [m2] , 0.0 [m2] , 25.0 [m2] ],'O')
>>>
Conceptually lengths2 represent the
same entity as lengths but :
·
lengths2 is a NumPy array, while lengths is a unum;
·
the storage needs will be much higher with lengths2 than with length;
·
the resource consumption (time and memory) for mathematical operations
will be much higher with lengths2 than with length.
The sole useful usage of this technique is when you have to mix, in the
same array, quantities with different dimensions or with different units. Note
that the behavior may become fuzzy if the two conventions are mixed in the same
expression; for example, if speed and speeds are defined as
above, then we have
>>>
lengths2 / speed
[0.3
[m] 0.4 [m] 0.0 [m] 0.25 [m] ] [s/m]
>>>
lengths2 / speeds
[0.272727272727
[m] 0.533333333333 [m] 0.0 [m]
2.5 [m] ] [s/m]
>>>
The results are unums, not NumPy arrays. There is no way for Unum to
propagate the unit [s/m] towards all the elements of the array because it
cannot guess the user's intention. The problem is easily solved by defining the
speeds as arrays of unums :
>>>
lengths2 / array([speed]*4)
array([0.3
[s] , 0.4 [s] , 0.0 [s] , 0.25 [s] ],'O')
>>>
speeds2 = array([22*M/S, 15*M/S, 24*M/S, 2*M/S])
>>>
lengths2 / speeds2
array([0.272727272727
[s] , 0.533333333333 [s] , 0.0 [s] ,
2.5 [s] ],'O')
>>>
We just covered here the basics of Numerical Python. In further
experiments, should the integration with Unum do not work as you expect it,
then, as shown above, the best is to invoke the method asNumber(…) with the
appropriate unit, in order to get back a true array After performing the
intended operation, the unit can easily been put back by multiplication.
Unum allows several customizations. These are controlled through a set
of parameters that are Unum's class variables. These may be changed at any time
-even during a running session- in order to change the default behavior. So a
lot of customizations may be done without hacking the Unum class source code.
The role of all these parameters is explained below.
Note : If you do not use the Unum calculator, you have to make the Unum
class visible explicitly :
>>>
from unum import Unum
>>>
The output formatting of unums can be changed by using the following
parameters :
UNIT_SEP |
separator between units (default = ".") |
UNIT_DIV_SEP |
separator between numerator and denominator (default = "/"); if set to None then negative exponents
are used |
UNIT_FORMAT |
output format string for unit group (default = "[%s]") |
UNIT_INDENT |
separator between value and units (default = " ") |
UNIT_HIDE_EMPTY |
boolean indicating that unit-less unums must be displayed as raw
numbers, i.e. without the string UNIT_FORMAT (default = False) |
UNIT_SORTING |
boolean indicating that units must be sorted alphabetically when
displayed; if False, then the order is unspecified and platform-dependant
(default = True) |
VALUE_FORMAT |
output format string for the value (default = "%s") |
Note that VALUE_FORMAT should be changed only for
scalar numerical values (e.g. "%15.7f"); it is
pointless for vectors and matrices, like in Numerical Python.
Here is an example of an alternative output formatting :
>>>
Unum.UNIT_SEP = ' '
>>>
Unum.UNIT_DIV_SEP = None
>>>
Unum.UNIT_FORMAT = '%s'
>>>
Unum.UNIT_HIDE_EMPTY = True
>>>
Unum.VALUE_FORMAT = "%15.7f"
>>>
M
>>> 1.0000000 m
>>>
25 * KG*M/S**2
25.0000000 kg m s-2
>>>
M/ANGSTROM
10000000000.0000000
>>>
The following exception classes are defined within Unum :
·
DimensionError : raised for any dimension
inconsistency (see ERR_UNIT
and ERR_EXP message strings below);
·
UnumError : raised for any
inconsistency, except dimension's (see ERR_BASIC, ERR_NOCONVERT and ERR_DUPLICATE message strings
below).
If needed, these exceptions may be caught by user applications. Here is a dumb
example within a Python interactive session :
>>>
from unum.units import *
>>>
from unum import Unum
>>>
try:
... M + KG
...
except Unum.DimensionError, exc:
... print exc
...
[m]
incompatible with [kg]
>>>
Error messages could be parameterized; this is especially useful for
using Unum in languages different from English. Here are the parameters with
their meaning (one must provide the right number of '%s' as shown in the
default strings) :
ERR_UNIT |
message associated to DimensionError exception, for
any units inconsistency in addition, subtraction, comparison or conversion
(default : "%s incompatible with %s") |
ERR_EXP |
message associated to DimensionError exception, indicating the presence of
unit(s) in exponents or mathematical functions (default : "unit %s unexpected") |
ERR_BASIC |
message associated to UnumError exception,
indicating that a unum refers to a non-basic units (default : "%s not a basic unit") |
ERR_NOCONVERT |
message associated to UnumError exception,
indicating the absence of a conversion unum (default : "%s has no conversion") |
ERR_DUPLICATE |
message associated to UnumError exception,
indicating that the same unit symbol is defined twice (default : "'%s' is already defined") |
By default, Unum will find the shortest unit representation among
equivalent expressions, by applying the known unit conversion rules. This is
called normalization. For example a pressure given in Pascal multiplied
by a surface will give a force in Newton, since one Pascal is equal, by
definition, to a Newton per square meter.
>>>
M/ANGSTROM
10000000000.0
[]
>>>
PA * M**2
1.0
[N]
>>>
PA * ANGSTROM**2
1e-020
[N]
>>>
If you want to avoid this normalization, you have to reset the boolean AUTO_NORM class variable.
>>>
Unum.AUTO_NORM = False
>>>
M/ANGSTROM
1.0
[m/angstrom]
>>>
PA * M**2
1.0
[Pa.m2]
>>>
PA * ANGSTROM**2
1.0
[Pa.angstrom2]
>>>
Then the normalization can be explicitly requested by calling the normalize method :
>>>
(PA * M**2).normalize()
1.0
[N]
>>>
This method actually normalizes the instance on which it is applied so
it has a permanent side-effect on the unum variables :
>>>
force = PA * M**2
>>>
force
1.0
[Pa.m2]
>>>
force.normalize()
1.0
[N]
>>>
force
1.0
[N]
>>>
This tutorial is based on Unum 4.0; running this version, you shouldn't
have any problem to run the examples exactly as described. Since previous
versions have been referred elsewhere (paper and poster), here are some notes
for compatibility problems. Most of the examples given in the tutorial will run
with Unum 1.x and 2.x. meanwhile the results may differ from those given. If
you want to use those versions, just type
from
unum import *
in the Python interpreter before typing the given session sample.
Here are the main features of each version.
- Unum 1.x is straight and easy to read but has annoying
functional lacks like automatic unit conversion; however, it is interesting to
understand Unum's main design ideas.
- Unum 2.x allows for automatic unit conversion but the design
is more complex. The unum module, beside the Unum class itself, contains
unnecessary or non-generic concepts, such as the definition of base units.
- Unum 3.0 essentially results from a streamlining of Unum 2.x :
the unum module becomes really generic since it doesn't
contain any definition of base units; these have been moved to ubase module, which
stands as a customizable sample unit database. A couple of bugs have been
corrected and the ease of customization has been improved.