EiffelUnits

EiffelUnits is a library of units of measurement for theEiffel language. It enables you to attach units to the quantities you use in your programs. EiffelUnits includes a collection of common units along with conversion, formatting and consistency checking utilities. EiffelUnits is extensible and can be adapted to user-defined unit systems outside the SI (e.g. currencies, color values, ...). I only compiled it with ISE Eiffel 5.1 - if you've tried it with other compilers, please tell me.

Contents: Scope  ¤  Usage  ¤  Problems  ¤  Glossary  ¤  References

Scope

Why units?

Units of measurement are an indispensable tool in many branches of science. Our ancestor's invention of pure numbers was certainly a big step in human history. The abstraction of sets out of incommensurable goods (17 geese and 4 bottles of wine make 21 ... guess what? ... objects , of course) is often useful . But we all know that abstract classes are not enough. At times, it's recommendable to categorize values into meaningful dimensions, such that we can better track and understand what we are calculating.

Our knowledge of natural laws stems partly from a powerful technique called Dimensional Analysis. Basically, dimensioned quantities are nothing more than values attributed with a dimension. By noting these dimensions explicitly, we can check whether our equations and calculations are meaningful. All natural laws discovered so far are dimensionally consistent, which leads us to the presumption, that there really is such a thing as a `dimension' in nature. We can employ the dimensions of the quantities we use as another means of ensuring correctness in our programs because dimensionally inconsistent calculations are definitely wrong (and thus raise an exception when incommensurable units are used). Dimensioned quantities are a tool which enables you to write systems that are better contracted in contrast to systems just using plain values.

The SI (système international d'unités; International System of Units)

SI's 7 base units and 2 supplementary units

Dimension Unit
SI base units:
Length (L) m (meter)
Mass (M) kg (kilogram)
Time (T)
s (second)
Electric Current (I) A (Ampère)
Thermodynamic Temperature (Theta) K (Kelvin)
Amount of Substance (N)
mol (mole)
Luminous Intensity (J)
cd (candela)
SI supplementary units:
Plane Angle (alpha) rad (radian)
Solid Angle (Omega)
sr (steradian)

The SI is an internationally agreed system of coherent units. It declares 7 base dimensions and exactly one unit per base dimension as a base unit, the core reference of scale in that dimension. No other (obsolete) unit should be used where SI units exist. A prominent example is meter , which is the SI base unit of base dimension length (L). Inch , mile, etc. are deprecated non-SI units. Base dimensions are orthogonal, in the sense that no base dimension can be expressed as a multiplicative combination of other base dimensions. Units can be prefixed by a set of standard multipliers (e.g. centi for10-2 or mega for 106).

Derived units are constructed as combinations of base units. Force for example has a derived dimension (M * L * T-2) whose unit kg * m * s-2 may also be written as N (Newton) .

There are two additional supplementary dimensions in the SI: the dimensions of plane and solid angles. The commitee seems not to have agreed on the nature of angular units. Even though there's little reason not to get angular units in the circle of base units, they are not "official" units. The mere fact that angular dimensions are often omitted is no justification for dropping them altogether. It is highly questionable whether we should really "merge" (meaning "confuse") the two fairly different notions of angle (a dimension with base unit rad) and dimensionless ratio ( angle / angle_of_full_revolution or length_of_circle_arc / circle_radius ). Therefore, EiffelUnits treats angular units like normal base units.

Usage

Cluster "examples" shows common usage patterns of EiffelUnits. Cluster "test" contains some test classes.

Client

Clients use the units and dimensions supplied with EiffelUnits. If your application stays within the SI unit system, it's likely that all the units you need are provided ready to use. Cluster unit.units holds classes with base dimensions and common units in these dimensions or powers thereof (e.g.m (meter) and l (liter) in class LENGTH, or s (second) and Hz (Hertz) in class TIME). Class COMPLEX_DERIVED provides some derived units with special names like N (Newton).

Class STANDARD_UNITS is a convenience class which includes all predefined units. The easiest way for clients to get access to EiffelUnits is by inheriting from STANDARD_UNITS. Another possibility (which doesn't clutter the namespace of your classes) is to define an attribute of type STANDARD_UNITS and access the units via a qualified call. Note that all classes defining units and dimensions are expanded. This allows clients to access their once-functions via an attribute without the need to create an object.

class MY_CLASS inherit
    STANDARD_UNITS
feature
    ... -- can directly use units

class MY_CLASS
feature
    unit: STANDARD_UNITS
    ... -- use units via `unit'.***

The primary notion for calculations with units is class QUANTITY. Enities representing quantities are declared using type QUANTITY:

altitude: QUANTITY
speed: QUANTITY

A QUANTITY object contains a DOUBLE value and a dimensionality. Quantities with equal dimensionality are said to be commensurate. Only commensurate quantities can be added, subtracted and compared to each other. These commensurateness conditions can be stated in contracts as shown here:

feature
    increase_altitude (by: QUANTITY) is
        require
            by.commensurate_with (m)
        deferred
        end

invariant
    altitude.commensurate_with (m)

QUANTITY is an expanded class whose entities can hold any unit or quantity. Yes, that's right: units are not special objects. They are just quantities which happen to carry some dimension. You usually don't have to "create" a unit for your quantity from scratch. Instead, you use once functions from classes in cluster unit.units and employ them as factors in multiplications and divisions with other quantity objects. Class QUANTITY_CONVERSION(which is an ancestor of STANDARD_UNITS, but can also be used directly) provides feature quantity (value: DOUBLE): QUANTITY to create dimensionless quantities:

feature
    lightspeed: QUANTITY is
        once
            Result := quantity (299792458) * m / s
        end

Temperatures in units other than Kelvin require special treatment. Instructions are given in class THERMODYNAMIC_TEMPERATURE.

Commensurability between quantities is defined as equality of their dimensions. Therefore, every QUANTITY has a DIMENSION attached, which is accessible through feature dimension. Class STANDARD_DIMENSIONS holds a set of commonly used dimensions, including the SI base dimensions. An instance of class STANDARD_DIMENSIONS is available via feature dim from class QUANTITY_CONVERSION, which you will most likely inherit if you intend to use EiffelUnits.

Picture of Atwood's MachineCase Study: Atwood's Machine

(This example is taken from [1].) Atwood's Machine is a simple mechanical device which consists of two unequal masses suspended by a string from a massless, frictionless pulley. The upward acceleration a of mass m1is given by

a = (m2 - m1) / (m2 + m1)

and the tension t in the string is given by

t = 2 * g * m1 * m2 / (m1 + m2)

where g is the magnitude of the acceleration due to gravity. These simple formulas are incorporated into the following class for Atwood's machine:

class
    ATWOODS_MACHINE

inherit
    MASS
    TIME
    LENGTH
   
create
    make
   
feature {NONE} -- Initialization

    make (mass_1_: QUANTITY; mass_2_: QUANTITY) is
            -- Load the machine
        require
            is_mass_1: mass_1_.commensurate_with (kg)
            is_mass_2: mass_2_.commensurate_with (kg)
        do
            mass_1 := mass_1_
            mass_2 := mass_2_
        end

feature -- Access

    mass_1: QUANTITY
    mass_2: QUANTITY
   
    acceleration_1: QUANTITY is
            -- acceleration of mass 1
        do
            Result := (mass_2 - mass_1) / (mass_1 + mass_2) * Gravity
        ensure
            Result.commensurate_with (m * s^-2)
        end

    tension: QUANTITY is
            -- tension in the string
        do
            Result := quantity (2) * (mass_1 * mass_2) / (mass_1 + mass_2) * Gravity
        ensure
            Result.has_dimension (dim.Force)
        end

feature -- Constants

    Gravity: QUANTITY is
            -- gravitational acceleration on earth surface
        once
            Result := quantity (9.81) * m / (s ^ 2)
        ensure
            Result.has_dimension (dim.Acceleration)
        end

invariant
    mass_1: mass_1.commensurate_with (kg)
    mass_2: mass_2.has_dimension (dim.Mass)

end -- class ATWOODS_MACHINE

Extension

If you need a derived dimension which is not defined in class STANDARD_DIMENSIONS, you can easily create it by multiplying and dividing base dimensions or existing derived dimensions. Remember that derived dimensions do not have to be "created" prior usage. They are generated on-the-fly as you execute calculations with existing units.

Sometimes, you need a new dimension for your special field of application. In this case, you simply create a new DIMENSION object, which becomes a new base dimension (orthogonal to all previous dimensions):

class MY_CLASS inherit
    QUANTITY_CONVERSION

feature

    My_dimension: DIMENSION is
        once
            create Result.make_new_base_dimension
        end
     
    my_base_unit: QUANTITY is
        once
            Result := base_quantity (My_dimension)
        end

New Units: Case Study "Astronomical Units"

See class LENGTH_ASTRONOMY in cluster root_cluster.examples to see how you could provide additional units or constants.

New Dimensions: Case Study "Fuel Consumption"

This example shows how to extend EiffelUnits. New base dimensions Gasoline and Distance are created to aid in consistency checks. New units l_gasoline and m_distance are defined on top of the new dimensions.

expanded class
    FUEL_CONSUMPTION

inherit
    LENGTH
       
feature -- Base Dimensions
    Gasoline: DIMENSION is
        once
            create Result.make_new_base_dimension
        end
       
    Gasoline_volume: DIMENSION is
        once
            Result := Gasoline * dim.Volume
        end
       
    Distance: DIMENSION is
        once
            create Result.make_new_base_dimension
            Result := Result * dim.Length
        end
       
    Fuel_consumption: DIMENSION is
        once
            Result := Gasoline_volume / Distance
        end

feature -- Units
    l_gasoline: QUANTITY is
        once
            Result := base_quantity (Gasoline) * l
        ensure
            definition: Result.is_equal (base_quantity (Gasoline) * l)
            Result.has_dimension (Gasoline_volume)
        end
       
    km_distance: QUANTITY is
        once
            Result := quantity (1000) * base_quantity (Distance)
        ensure
            definition: Result.is_equal (quantity (1000) * base_quantity (Distance))
            Result.has_dimension (Distance)
        end
end -- class FUEL_CONSUMPTION

indexing
    description: "A Car with an average fuel consumption"

class CAR inherit
    FUEL_CONSUMPTION

create
    make

feature -- Initialization

    make (consumption_: QUANTITY) is
            -- Set fuel consumption to `consumption_'
        require
            consumption_.has_dimension (Fuel_consumption)
        do
            consumption := consumption_
        ensure
            consumption_set: consumption = consumption_
        end

feature -- Access
   
    km_for_tank (tank_contents: QUANTITY): QUANTITY is
            -- how far can current car go?
        require
            tank_contents.commensurate_with (l_gasoline)
        do
            Result := tank_contents / consumption
        ensure
            Result.commensurate_with (km_distance)
        end

feature -- Status report

    consumption: QUANTITY
        -- average fuel consumption

invariant
    consumption.has_dimension (Fuel_consumption)

end -- class CAR

Problems

Units are not statically checked. Unfortunately it was not possible to implement unit checking statically (at compile time). In complex unit expressions, we have to deal with a great many unforeseeable compound units. It is not feasible to manually provide classes and operations for all possible combinations of units. Furthermore, that approach would not be extensible.
In Eiffel, types are based either directly on classes or on generic derivations of generic classes. Since types are not computable on demand at compile time, I don't see a possibility to implement static unit checking. Please inform me if you disagree and have a solution. But remember: we don't want preprocessors or compiler hacks. Just a clean Eiffel library.
The increased safety gained with unit checking is currently traded in for a runtime overhead over unchecked programs.

Persistence of quantities is not supported. Base dimensions are currently identified in once functions by a monotonous increasing counter. Since the execution sequence of once functions is not neccessarily the same in different sessions, dimensions are not guaranteed to be assigned the same identifiers between runs. Therefore, quantity objects can not be stored and retrieved safely between different executions of a program by means of STORABLE.
A possible solution: identify a base dimension by its symbol (e.g. "L" for length), stored in some global place. But that would not statically ensure uniqueness.

EiffelUnits is based on ISE EiffelBase. I apologize for any inconveniences in terms of portability.

All values are DOUBLE. EiffelBase has weak support for generic usage of numeric data types. INTEGER and DOUBLE both inherit independently from NUMERIC and from COMPARABLE. Hence there's no common ancestor that clients can use if they don't care about the exact data type (be that INTEGER, DOUBLE, RATIONAL, BIG_INTEGER, etc.). The following class declaration is not legal in Eiffel: class QUANTITY [G -> {NUMERIC, COMPARABLE}] inherit ...

Conversion of DOUBLE value to QUANTITY. At the time of writing, there's no automatic object conversion in Eiffel. Therefore, you have to use feature quantity (v: DOUBLE): QUANTITY from classQUANTITY_CONVERSION to make the transition from (manifest) numbers to quantities. The reverse way is feature in (unit: QUANTITY): DOUBLE from class QUANTITY. 

Known Bugs. There are no known bugs at this time. If you find one, please crunch it and send it to me. Thank you.

Glossary

Base dimensions:
A set of dimensions (physical properties) which are assumed to be mutually independent. A base dimension can not be expressed by any combination of the other base dimensions. The base dimensions of the SI are listed in the table above.
 
Derived dimension:
A dimension that results from a product or quotient of two or more base dimensions. "Derived" is the nomenclature specified by the SI.
 
Dimension:
A measurable or calculable physical property, for example energy. SI uses the term "quantity" for this concept. A dimension may be a base dimension or a derived dimension.

Quantity (dimensioned quantity):
A numeric value together with its dimension.

Unit:
A Quantity which defines a reference of scale for measurements in a certain dimension. SI specifies one base unit for each base dimension. Certain derived dimensions have a base unit too (e.g. dimension force has base unit N, which stands for m * kg * s-2).

References

[1] John J. Barton and Lee R. Nackman: "Scientific and engineering C++: an introduction with advanced techniques and examples". Reading, Massachusetts, Addison-Wesley, 1994.

Further information on units of measurement and standards can befound here:


Copyright ©2002 Markus Keller, markus_keller@student.ethz.ch
This library is free for use and redistribution by everybody.
It resulted from a semester project at ETH Zürich ,Chair of Software Engineering
Last change: 2002-10-08