*Unum* *Tutorial**//* by Pierre X. Denis /pierre.denis@spacebel.be / / / Spacebel, Belgium last updated - November 4, 2003 ____________________________________________ *Table of Contents* ____________________________________________ _Introduction <#_Toc55401987>_ Installation <#_Toc55401988> Starting a session with Unum <#_Toc55401989> Working With Unums <#_Toc55401990> Consistency Checking <#_Toc55401991> Unit Conversions <#_Toc55401992> Integration with mathematical functions <#_Toc55401993> Integration with Python's types <#_Toc55401994> Predefined Units <#_Toc55401995> Unit Catalog Functions <#_Toc55401996> A Small Application <#_Toc55401997> How To Define New Units <#_Toc55401998> Alternative Ways To Define Units <#_Toc55401999> Persistence <#_Toc55402000> Integration with Numerical Python <#_Toc55402001> Unum Customization <#_Toc55402002> Note About Older Unum versions <#_Toc55402003> ____________________________________________ 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 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 2. In your terminal, type cd python setup.py install 3. if the installation is successful, you can safely remove This will install Unum packages in your Python site-packages directory, i.e. it will create the directory /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 /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 /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 <#_Some_Useful_Functions>). Actually, the use of this import statement is advised to embed unums in applications (see A Small Application <#_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 <#_Toc510971939>. Note that any unit can be (re-) defined according to the specific needs of your application domain (see section How To Define New Units <#_Toc510971939>). 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 <#_Integration_with_mathematical>). Note that unit output formatting is customizable (see Unum Customization <#_Output_formatting>); 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 <#_Normalization>). 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(DEGREE) 360.0 [°] >>> 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*DEGREE) -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 <#_Normalization>). 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 <#_Integration_with_Numerical>). 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 <#_Some_Useful_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 have been adapted because their unit symbol is not a valid variable name : *_variable name_* *__* *_symbol_* *_name_* ANGSTROM [angstrom] angstrom, official symbol [Å] CELSIUS [°C] degree Celsius (temperature unit) DEGREE [°] degree (angle unit) MINUTE ['] minute (angle unit) OHM [ohm] ohm, official symbol [W] SECOND [''] second (angle unit) 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] >>> Ø Be careful on some misleading unit homonyms : *_variable name_* *__* *_symbol_* *_name_* S [s] second (time unit) SECOND [''] second (angle unit) MIN [min] minute (time unit) MINUTE ['] minute (angle unit) CELSIUS [°C] degree Celsius (temperature unit) DEGREE [°] 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 [°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 : /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 static method unit defined in 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 that, since these are Unum's class variables, you have to make the Unum class visible : >>> from unum import Unum >>> 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] >>> 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 an denominator (default = '/'), if set to None then negative exponents are used UNIT_FORMAT format of unit groups (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) 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 >>> M 1.0 m >>> 25 * KG*M/S**2 25.0 kg m s-2 >>> M/ANGSTROM 10000000000.0 >>> Error Messages and Exceptions Error messages could be parameterized; this is especially useful for using Unum in languages different from english. Here are the parameters with their meaning : 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") One must provide the right number of '%s' as shown in these strings. 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.