Unum

Tutorial

 

by Pierre X. Denis

pierre.denis@spacebel.be

 

Spacebel, Belgium

 

 

last updated - March 24, 2004

 


____________________________________________

Table of Contents

____________________________________________

Introduction

Installation

Starting a session with Unum

Working With Unums

Consistency Checking

Unit Conversions

Integration with mathematical functions

Integration with Python's types

Predefined Units

Unit Catalog Functions

A Small Application

How To Define New Units

Alternative Ways To Define Units

Persistence

Integration with Numerical Python

Unum Customization

Note About Older Unum versions

 

____________________________________________

 

Introduction

 

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.

 

Installation

 

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.

 

Starting a session with Unum

 

There are basically two ways you can use Unum to make interactive calculations.

 

  1. You start the Unum calculator : it is an interactive Python interpreter that is Unum-aware; it allows to make calculations with all the SI units and some others. To start the Unum calculator, type the following

               
    python <python-site-packages-dir>/unum/tools/calc.py

    (or use any handy shortcut, alias, … available in your OS). The following message should be displayed

               
    -- Welcome in Unum Calculator (ver 04.00) --
         >>>

    This starts an environment very similar to the Python interpreter with the nice >>>  prompt awaiting your first Unum command. Note that the Unum calculator is quitted in a similar way than Python interpreter, i.e. by typing an EOF character (e.g. Control-D on Unix, Control-Z on Windows) + Newline. Besides Unum expressions, any valid Python statement -included import- will be executed exactly as in the standard interpreter.

  2. You can also work with the Unum calculator, by importing units directly in a running Python session :

>>> 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). 


Working With Unums

 

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

>>>

 

Consistency Checking

 

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.

 

Unit Conversions

 

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.

 

Integration with mathematical functions

 

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

>>>

 

Integration with Python's types

 

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).

 

 

Predefined Units

 

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
+ user-defined units

 

 

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]

>>>

 

Unit Catalog Functions

 

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.

 

A Small Application

 

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]

 

 

How To Define New Units

 

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]

>>>

 

Alternative Ways To Define Units

 

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.

 

Persistence

 

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.

 

Integration with Numerical Python

 

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 Customization

 

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

>>>

 

Output formatting

 

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

>>>

 

Exceptions and Error Messages

 

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")

 

 

Normalization

 

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]

>>>

 

 

Note About Older Unum versions

 

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.