All Gems »

Filter by Products
Next Next

# Gem #136: How tall is a kilogram?

Let's get started...

This Gem outlines the new GNAT dimensionality checking system. This feature relies on Ada 2012 aspect specifications, and is available from version 7.0.1 of GNAT onwards.

The GNAT compiler now supports dimensionality checking. Ever since the appearance of user-defined operators in Ada 83 there have been attempts to use these to declare types with dimensions, and perform dimensionality checks on scientific code. These attempts were unfortunately unwieldy and were not adopted by the language. The system we describe here is lightweight and relies on the aspect specifications introduced in Ada 2012. We hope that the engineering and scientific community will find it convenient and easy to use.

Before exploring the implementation of this feature, let's first consider a common example in physics: the free fall problem. We want to evaluate the distance traveled by a body in 10 seconds of free fall knowing the gravitational acceleration on Earth. The well-known solution is:

distance = 1/2 * g * t**2 where t = 10s.

A question arises here : how do we define dimensioned objects in Ada?

For this purpose, The GNAT compiler provides the International System of Units, i.e., the meter-kilogram-second (MKS) system of units, in package System.Dim.Mks. GNAT also provides output facilities for this system of units in package System.Dim.Mks_IO.

The package System.Dim.Mks defines a numeric base type Mks_Type, and subtypes of it that correspond to physical quantities (such as Length, Time) as well as constants of each dimensioned quantity (such as m, s) are defined.

Using these two packages, we are now able to convert our free fall problem into Ada code:

```   with System.Dim.Mks; use System.Dim.Mks;
with System.Dim.Mks_IO; use System.Dim.Mks_IO;
with Text_IO; use Text_IO;

procedure Free_Fall is

G        : constant Mks_Type := 9.81 * m / (s**2);
T        : Time := 10.0*s;
Distance : Length;

begin
Put ("Gravitational constant: ");
Put (G, Aft => 2, Exp => 0); Put_Line ("");
Distance := 0.5 * G * T ** 2;
Put ("distance traveled in 10 seconds of free fall: ");
Put (Distance, Aft => 2, Exp => 0);
Put_Line ("");
end Free_Fall;

```

Compiling and running our example yields:

```   \$ gnatmake -q -gnat2012 free_fall.adb
\$ ./free_Fall```
```   Gravitational constant: 9.81 m.s**(-2)
distance traveled in 10 seconds of free fall: 490.50 m```

The properties of type Mks_Type and its subtypes Time and Length are defined by means of two new GNAT-specific aspects:

The aspect Dimension_System allows the user to define a system of units. The aspect Dimension_System can only apply to a numeric type, typically a floating point type of the appropriate precision (any numeric type can be used as a base). A type (more accurately a first subtype) to which the aspect Dimension_System applies is a dimensioned type. The syntax of an aspect Dimension_System is as follows:

with Dimension_System => ( DIMENSION {, DIMENSION});

DIMENSION ::= ( [Unit_Name =>] IDENTIFIER, [Unit_Symbol =>] SYMBOL, [Dim_Symbol =>] SYMBOL)

SYMBOL ::= STRING_LITERAL | CHARACTER_LITERAL

That is to say, the value of the aspect is an aggregate. Up to 7 dimensions (corresponding to the International System of Units) can be specified.

The aspect Dimension allows the user to declare dimensioned quantities within a given system. An aspect Dimension applies only to a subtype of a dimensioned type. A subtype to which the aspect Dimension applies is a dimensioned subtype. The syntax of an aspect Dimension is as follows:

with Dimension => ( [[Symbol =>] SYMBOL,] DIMENSION_VALUE {, DIMENSION_VALUE});

SYMBOL ::= STRING_LITERAL | CHARACTER_LITERAL

DIMENSION_VALUE ::= RATIONAL | others => RATIONAL | DISCRETE_CHOICE_LIST => RATIONAL

RATIONAL ::= [-] NUMERAL [/NUMERAL]

Using both aspects, users are thus able to define their own systems of units and to compute dimensioned formulae. The compiler will diagnose dimension mismatch in computations and assignments.

Now, let's have a closer look at the MKS package. The MKS dimensioned type is defined as follows:

```   type Mks_Type is new Long_Long_Float
with
Dimension_System => (
(Unit_Name => Meter,    Unit_Symbol => 'm',   Dim_Symbol => 'L'),
(Unit_Name => Kilogram, Unit_Symbol => "kg",  Dim_Symbol => 'M'),
(Unit_Name => Second,   Unit_Symbol => 's',   Dim_Symbol => 'T'),
(Unit_Name => Ampere,   Unit_Symbol => 'A',   Dim_Symbol => 'I'),
(Unit_Name => Kelvin,   Unit_Symbol => 'K',   Dim_Symbol => "Theta"),
(Unit_Name => Mole,     Unit_Symbol => "mol", Dim_Symbol => 'N'),
(Unit_Name => Candela,  Unit_Symbol => "cd",  Dim_Symbol => 'J'));

```

A dimensioned subtype (such as Length, Mass, Time, Electric_Current, Thermodynamic_Temperature, Amount_Of_Substance, Luminous_Intensity) is defined as follows:

```   subtype Length is Mks_Type
with Dimension => (Symbol => 'm',
Meter  => 1,
others => 0);

```

The package also defines conventional names for values of each unit such as:

```   m  : constant Length := 1.0;
cm : constant Length := 1.0E-02;
```

In both Dimension and Dimension_System aspects, symbols passed as arguments are used for output purposes. Indeed, GNAT also provides dimension-specific output facilities in two generic packages for both integer- and float-dimensioned types: System.Dim.Integer_IO. and System.Dim.Float_IO. The Put routines defined in these packages output the numeric value, followed by the dimension vector of the item passed as parameter (System.Dim.Mks_IO, used in the free fall example, is an instance of System.Dim.Float_IO instantiated with the Mks_Type in package System.Dim.Mks).

Back to the free fall example, the careful reader will have noticed that the gravitational constant G could be defined by introducing a new dimensioned subtype:

```   subtype Acceleration is Mks_Type
with Dimension => ("m/sec^2
Meter  => 1,
Second => -2,
others => 0);
G : constant Acceleration := 9.81 * m / (s**2);

```

The execution of the free_fall program now yields:

`   \$ ./free_fall`
```   Gravitational constant: 9.81 m/sec^2
distance traveled in 10 seconds of free fall: 490.50 m```

The purpose of dimensional analysis is to verify dimension consistency within physical relations, and prevent the kind of errors that have become legendary, such as the space probe that was lost because one module computed in metric units and another one in Imperial units. GNAT checks dimension consistency on assigments, parameter passing, addition operations, and comparisons. GNAT computes the resulting dimensions of multiplication and exponentiation operations.

For instance, in the free fall example, an incorrect assignment such as:

```   Distance := 5.0 * kg;

```

is rejected at compile time with the following diagnoses:

\$ ./free_fall

free_fall.adb:15:13: dimensions mismatch in assignment

free_fall.adb:15:13: left-hand side has dimension [L]

free_fall.adb:15:13: right-hand side has dimension [M]

Incorrect arithmetic operations also lead to such diagnoses. For example, in the free fall program:

```   Distance := T + G;
```

is rejected as follows: \$ ./free_fall

free_fall.adb:15:18: both operands for operation "+" must have same dimensions

free_fall.adb:15:18: left operand has dimension [T]

free_fall.adb:15:18: right operand has dimension [L.T**(-2)]

This introduction should be sufficient for the user to start experimenting with the existing packages on engineering/scientific code. The next step for the ambitious reader is to create a new system of units, such as the centimeter-gram-second (CGS) system, and test as many physical formulae as possible in order to get familiarized with this new feature. If multiple systems are present, it will be natural to introduce conversion routines between them. Because the base types of different systems are distinct, type checking ensures that these values cannot be accidentally mixed in the same computation.

This is a new feature of the GNAT tool chain. We welcome comments from users, suggestions for improvements, and interesting examples of use!

Vincent Pucci
AdaCore

## 16 Comments

• Ken Elsom
Nov 12th, 2012

I was surprised that the System package was called MKS.  SI units replaced MKS about 40 years ago.  That is the international standard and that is what you should use.

• Anh Vo
Nov 13th, 2012

Compliling the free_fall.adb example using command ‘gnatmake -q -gnat2012 free_fall.adb’, the compilation fails as the result of errors at lines 1 and 2. It complains that System.Dim is not a predefined library unit. Both GNAT Pro 7.0.1 and 7.0.2 behave the same way. In a similar situation, other versions of GNAT just give Warnings. I am wondering why GNAT Pro treats this as error.

• Anh Vo
Nov 13th, 2012

I just checked on GNAT Pro 7.0.1 and GNAT Pro 7.0.2. Neither of these compilers has System.Dim* packages implemented.

• Anh Vo
Nov 14th, 2012

The program Free_Fall compiles successfully under gnat-7.1-preview. Just to repeat, this program fails to compile under gnat-7.0.1 and gnat-7.0.2.

• Anh Vo
Nov 15th, 2012

The example Free_Fall failed to compile under gnat-7.0.1 and gnat-7.0.2. Howerver, it compiled fine with gnat-7.1-preview.

• M. Yaas Automacio
Dec 6th, 2012

I agree with Ken Elsom above. The attempts from the computer science people is laudable, but there are many, many cases where physicists, chemists, etc. get frustrated and pass up opportunities from the computer science community because it is cultural for computer scientists (software algorithm people) to downplay the importance of STANDARD NOTION and STANDARD TERMS. They habitually have this urge to RENAME things and that’s the problem. Not only the MKS versus the SI (modern), but this extends elsewhere as well. Computer science people should consult physicists and mathematics people before deciding to “label” something that is suppose to be used in physics, and algebra, and calculus,etc.

• mg
Feb 15th, 2013

At last!! Numbers by themselves have no meaning. It is the dimension that tells us what the quantity is about: it’s never the same 2 kisses than 2 blows.

• Daniel Bigelow
May 1st, 2013

The motivation for compiler-level support of dimensionality checking is of course program correctness. Therefore, I was surprised to see your example using positional notation which is inherently error-prone and also appears to employ a hack to step over the “Kilogram” row in MKS dimensioned type to reach the “Second” row. You wrote

subtype Acceleration is Mks_Type
with Dimension => (“m/sec^2”, 1, 0, -2, others => 0);

But, I think the example would be better served using named notation as in
subtype Acceleration is Mks_Type
with Dimension => (Symbol => “m/sec^2”,
Meter => 1,
Second => -2,
others => 0);

• Gary Dismukes
May 18th, 2013

Daniel,

Agreed that using named notation is a better choice here, and the example has been revised accordingly.  As it happens, the same subtype appears in the GNAT User Guide using named
notation (and in another example with positional notation).

• Henri GEIST
Apr 2nd, 2014

Very interesting feature !

But I had stick the ‘Dimension_System’ aspect to the package and the ‘Dimension’ aspect to numerics types declared in the package or its childrens rather than the ‘Dimension_System’ aspect to the type and the ‘Dimension’ to its subtypes.

As people using fixed point arithmetic will use different types for different units adapting precision and range to the different dynamic value.
You will rarely encounter Tera Farad or pico Hertz in real application.
And no fixed point type can have a ‘Capacity_T’ subtype working with pF and an other
‘Frequency_T’ subtype working with THz.
It will never fit in 64 bits. And even if it has been possible for performance reason it is no desirable.

It could be esayest to declare

package My_SI_Units
with Dimension_System => (
(Unit_Name => Meter,  Unit_Symbol => ‘m’,  Dim_Symbol => ‘L’),
(Unit_Name => Kilogram, Unit_Symbol => “kg”,  Dim_Symbol => ‘M’),
(Unit_Name => Second,  Unit_Symbol => ‘s’,  Dim_Symbol => ‘T’),
(Unit_Name => Ampere,  Unit_Symbol => ‘A’,  Dim_Symbol => ‘I’),
(Unit_Name => Kelvin,  Unit_Symbol => ‘K’,  Dim_Symbol => “Theta”),
(Unit_Name => Mole,    Unit_Symbol => “mol”, Dim_Symbol => ‘N’),
(Unit_Name => Candela,  Unit_Symbol => “cd”,  Dim_Symbol => ‘J’));
is

type Capacity_T is delta 10.0**(-12) range -10.0 .. 10.0
with Dimension => (Symbol => “F”,
Meter => -2,
Second => 4,
Ampere => 2,
Kilogram => -1,
others => 0);

type Frequency_T is delta 0.1 range -10.0**(15) .. 10.0**(15)
with Dimension => (Symbol => “Hz”,
Second => -1,
others => 0);

End My_SI_Units;

• Vincent Pucci
Apr 3rd, 2014

Henri,

Thank you for your feedback. This is always very helpful. Plus, I really like your proposal!

However the decision to attach an aspect Dimension_System to a type (rather than a package) has been taken in order to let the user juggling with dimensioned objects without using explicit casts.

As an illustration, consider, with your proposal the following declarations:

type Length is delta 1.0 range 10.0**(6)
with Dimension => (Meter => 1,
others => 0);

type Time is delta 1.0 range 0.0 .. 10.0**(5)
with Dimension => (Second => 1,
others => 0);

type Speed is delta 0.1 range 0 .. 10.0**(2)
with Dimension => (Meter => 1,
Seconds => -1,
others => 0);

Distance       : constant Length := 1.0E+4;
Elapsed_Time : constant Time := 3600.0;

To calculate the speed:

Result : constant Speed := Speed (Long_Float (Distance) * Long_Float (Elapsed_Time));
— really frustrating and inconvenient for the user.

This notation reveals to be very awkward and not really user-friendly.

However the possibility to deal with several and different dimensioned numeric types should be considered and put on the road map for future versions.

• Henri GEIST
Apr 3rd, 2014

Vincent

Thank you for your answer.

I am sorry may be I missed something but I do not see any cast problems.

At leas this code snippet compile on my computer :

procedure Main is
type Length is delta 1.0 range 0.0 .. 10.0**(6);
—  with Dimension => (Meter => 1,
—              others => 0);
type Time   is delta 1.0 range 0.0 .. 10.0**(5);
—  with Dimension => (Second => 1,
—              others => 0);
type Speed is delta 0.1 range 0.0 .. 10.0**(12);
—  with Dimension => (Meter   =>  1,
—              Seconds => -1,
—              others =>  0);

Distance   : constant Length := 1.0E+4;
Elapsed_Time : constant Time   := 3600.0;
Result     : constant Speed := Distance * Elapsed_Time;
begin

null;

end Main;

No need to cast anything, no awkward notation, no frustration.

Anyway casting to Long_Float will defeat the prurpose of using fixed point type and should not be done.

And In my own case the cast is even prohibited by the fact I always use
pragma Restrictions (No_Floating_Point);

P.S. In your examples you had some typos :
- range of Length had no First value.
- First value of Speed range should be Universal_Real.
- Last value of Speed range was to small to handle the Result constant.

• Vincent Pucci
Apr 3rd, 2014

Henri,

Good point, I got confused between floating point and fixed point types… sorry

Indeed, we would need explicit casts for mixing floating point types but it makes perfectly sense to allow that kind of constructs and operations on dimensioned fixed point objects.

That clearly needs to be investigated and to be taken into account in our road map.

Thank you again!

• Wilhelm Spickermann
May 28th, 2014

Many of the dimensioned objects are vectors which can only be reduced to scalars in simple cases. If we add an initial horizontal speed to the example given in the article we get

distance = 1/2 * g * t**2 + speed * t
with “speed”, “g” and “distance” being arrays of the corresponding subtypes.

Can this be done without defining new operations for every combination of dimensioned array types?

• Vincent Pucci
May 28th, 2014

Hi Wilhelm,

First, thank you for your feedback.

As you pointed out, the free fall example presented in this gem only only deals with scalar objects whereas many calculations in physics require vectors and matrices.
The use of dimensioned vectors and matrices in the GNAT dimensionality checking system do not require additional operations and is pretty straight-forward.

I rewrote the free fall example with “speed”, “g” and “distance” being arrays.
the following code compiles and runs successfully:

with System.Dim.Mks; use System.Dim.Mks;
with System.Dim.Mks_IO; use System.Dim.Mks_IO;
with Text_IO; use Text_IO;

procedure Free_Fall is
T : constant Time := 10.0*s;
— the simulation elapsed time.

Nbr_Of_Sim : constant Positive := 9;
— the number of different free_fall simulations to be run.

subtype Acceleration is Mks_Type
with Dimension => (Meter =>  1, Second => -2, others =>  0);

G_Earth : constant Acceleration := 9.81 * m / (s**2);
G_Jupiter : constant Acceleration := 26.0 * m / (s**2);
G_Pluto : constant Acceleration := 0.61 * m / (s**2);

G : constant array (1 .. Nbr_Of_Sim) of Acceleration :=
(1 .. 3 => G_Earth,
4 .. 6 => G_Jupiter,
others => G_Pluto);
— the gravitational acceleration to considered for each simulation.

Speeds : constant array (1 .. Nbr_Of_Sim) of Speed :=
(1 | 4 | 7 => 0.0 * km / hour,
2 | 5 | 8 => 10.0 * km / hour,
others   => 100.0 * km / hour);
— the initial horizontal speed of each simulation.

Distances : array (1 .. Nbr_Of_Sim) of Length;
— the distance traveled at elapsed time.

begin
for s in Distances’Range loop
Distances (s) := 0.5 * G (s) * T ** 2 + Speeds (s) * T;
end loop;

Put_Line (”—distance traveled in 10 seconds of free fall—”);
for s in Distances’Range loop
Put (“simulation” & s’Img & “: “);
Put (Distances (s), Aft => 2, Exp => 0);
Put_Line (”“);
end loop;
end Free_Fall;

• Wilhelm Spickermann
May 28th, 2014

Hi Vincent,

thanks for the immediate answer.

We’ve had a misunderstanding here. The arrays in your example are just collections of scalars for several examples and there are no vector operations in the formulasnot components in three dimensional space. Consequently, the program calculates the result for a vertical initial speed and not for a horizontal. I was talking about something like this (which does not work):

subtype Acceleration is Mks_Type
with Dimension => (Meter =>  1, Second => -2, others =>  0);

type Acceleration_Vector is array (1 .. 3) of Acceleration;
— components: 1: North, 2: East, 3: down

G : constant Acceleration_Vector := (0.0 * m / (s**2),
0.0 * m / (s**2),
9,8 * m / (s**2));

type Speed_Vector is array (1 .. 3) of Speed;

Speed : constant Speed_Vector := (0.0 * m / s,
5.0 * m / s,
0.0 * m / s);

...
Distance : Length_Vector;
...

Distance := 0.5 * G * T ** 2 + Speed * T;

Comments on Gems are now closed.