!standard A.16 (01) 03-02-01 AI95-00324/01

!class amendment 03-02-01

!status work item 03-02-01

!status received 03-02-01

!priority Medium

!difficulty Medium

!subject Physical Units Checking

!class amendment 03-02-01

!status work item 03-02-01

!status received 03-02-01

!priority Medium

!difficulty Medium

!subject Physical Units Checking

!summary

A new set of packages and generic packages is proposed
that provide compile-time checking for calculations
involving physical units.

!problem

Ada is one of the very few languages that provides strong
type checking for numeric types. Unfortunately, the checking
Ada provides for multiplicative operators is often not
correct when performing calculations involving physical
quantities. Such quantities are generally measured using
units such as centimeters, grams, seconds, miles-per-hour, etc.
Multplication and division are only meaningful when the
units "work out" properly. Exponentiation is of course
only meaningful when the corresponding multiplication
would have been meaningful.

!proposal

A new set of packages and generic packages is proposed,
all in a new hierarchy "Ada.Units.*", which together
provide support for compile-time checking of calculations
involving physical quantities.

Note: This is a joint proposal from Tucker Taft and
Thom Brooke.

-----

The package Ada.Units forms the root of the hierarchy devoted
to physical units checking.

Exponent_Type is used to define the exponent for a physical
unit along each dimension (length, mass, time, charge, etc.).

-----

The generic package Ada.Units.Dimension_Exponents defines the Exponent_Array
type, which is used throughout to describe the dimensions associated with a
physical unit.

-- NOTE: It is recommended that each enumeration literal
-- be a generic dimension name like "length" or "time" so it
-- won't collide with the name of a specific unit such as "cm"
-- which would be the typical name for an instance of the
-- "Unit" generic.

Number_Of_Dimensions : **constant** Integer := Exponent_Array'Length;

The enumeration type Names_Of_Dimensions provides
names for the various dimensions in the system of units being
defined. Typically, this type might be defined with the
enumeration literals such as "Length", "Mass", "Time", etc.

The operators on the exponent array type are used within
Assert pragmas to verify that the units passed
into a generic are related appropriately.

-----

The package Ada.Units.Name_Support provides a set of default name
construction utilities which are used in the absence of an explicit
specification of a name for a "scaled" unit, a "product" unit, or a
"quotient" unit. See System_Of_Units below for what it means to be a
scaled, product, or quotient unit.

-----

The generic package Ada.Units.System_Of_Units provides the primary
support for physical units checking. It provides nested
generics for creating particular units that together form
a single "system" of units, such as "CGS" (cm, g, sec) or "MKS"
(meter, kg, sec). A typical use would first instantiate
System_Of_Units, and then instantiate the nested generic
Unit, once for each unit of interest, such as "cm" or
"g" or "kph". See the examples for further understanding.

-- Names_Of_Dimensions should be an enumeration type;
-- Each enumeration literal corresponds to one dimension
-- in the system of units.
-- Value_Type is floating-point type that will be parent type
-- for all types declared in this package.

-- Define other parameters used in signature package
Name : **constant** String := "(unitless)";
Exponents : **constant** Exponent_Array := (**others** => 0);
Scale_Factor : **constant** Scale_Type := 1.0;

-- Define a type to be used as a base for further derivations
-- and provide the operators that combine with the unitless type
**type** Base_For_Derivation **is** **new** Value_Type'Base;
**function** "*"(Left : Base_For_Derivation; Right : Unitless.Value)
**return** Base_For_Derivation;
**function** "*"(Left : Unitless.Value; Right : Base_For_Derivation)
**return** Base_For_Derivation;
**function** "/"(Left : Base_For_Derivation; Right : Unitless.Value)
**return** Base_For_Derivation;
**function** "/"(Left : Base_For_Derivation; Right : Base_For_Derivation)
**return** Unitless.Value;

-- And make the predefined multiplicative operators abstract
**function** "*"(Left, Right : Base_For_Derivation)
**return** Base_For_Derivation **is** **abstract**;
**function** "/"(Left, Right : Base_For_Derivation)
**return** Base_For_Derivation **is** **abstract**;
**function** "**"(Left : Base_For_Derivation; Right : Integer)
**return** Base_For_Derivation **is** **abstract**;

-- This package is for units that have some non-zero dimension
-- See Dimensionless_Unit below for case
-- where exponents are all zero
**pragma** Assert(Exponents /= Unitless.Exponents);

-- Scale_Factor of base unit is always 1.0
Scale_Factor : **constant** Scale_Type := 1.0;

-- Note that this is instantiated as part of
-- defining a scaled unit below, providing
-- scaling functions between the base unit
-- and the scaled unit. Instantiate this directly
-- to get additional scaling functions between two scaled units
-- (with the same base unit).

-- Define other parameters to signature
Scale_Factor : **constant** Scale_Type :=
Relative_Scale * Base_Unit.Scale_Factor;

Exponents : Exponent_Array **renames** Base_Unit.Exponents;

-- And rename them for direct visibility
**function** Scale(From : Base_Unit.Value) **return** Scaled_Unit.Value
**renames** Scaling.Scale;
**function** Scale(From : Scaled_Unit.Value) **return** Base_Unit.Value
**renames** Scaling.Scale;
-- also note that unary "+" can be used for this scaling
-- (see below)

-- Define the appropriate additive operators
-- NOTE: Except for unary "+", these all return Base_Unit.Value to
-- avoid ambiguity in complicated expressions.
**function** "+"(Left : Base_Unit.Value; Right : Scaled_Unit.Value)
**return** Base_Unit.Value;
**function** "+"(Left : Scaled_Unit.Value; Right : Base_Unit.Value)
**return** Base_Unit.Value;
**function** "+"(Right : Scaled_Unit.Value)
**return** Base_Unit.Value **renames** Scaling.Scale;
-- Unary "+" can be used for scaling.

-- provide other parameters of signature
Scale_Factor : **constant** Scale_Type :=
Unit_A.Scale_Factor * Unit_B.Scale_Factor;

Exponents : **constant** Exponent_Array :=
Unit_A.Exponents + Unit_B.Exponents;

-- Get appropriate multiplicative operators
**package** Ops **is** **new** Multiplicative_Operators(
Unit_A, Unit_B, Product_Unit.Signature);

-- Rename operators for direct visibility
**function** "*"(Left : Unit_A.Value; Right : Unit_B.Value)
**return** Product_Unit.Value **renames** Ops."*";
**function** "*"(Left : Unit_B.Value; Right : Unit_A.Value)
**return** Product_Unit.Value **renames** Ops."*";
**function** "/"(Left : Product_Unit.Value; Right : Unit_A.Value)
**return** Unit_B.Value **renames** Ops."/";
**function** "/"(Left : Product_Unit.Value; Right : Unit_B.Value)
**return** Unit_A.Value **renames** Ops."/";

-- provide other parameters of signature
Scale_Factor : **constant** Scale_Type :=
Unit_A.Scale_Factor / Unit_B.Scale_Factor;

Exponents : **constant** Exponent_Array :=
Unit_A.Exponents - Unit_B.Exponents;

-- Get appropriate multiplicative operators
**package** Ops **is** **new** Multiplicative_Operators(
Quotient_Unit.Signature, Unit_B, Unit_A);

-- Rename operators for direct visibility
**function** "*"(Left : Quotient_Unit.Value; Right : Unit_B.Value)
**return** Unit_A.Value **renames** Ops."*";
**function** "*"(Left : Unit_B.Value; Right : Quotient_Unit.Value)
**return** Unit_A.Value **renames** Ops."*";
**function** "/"(Left : Unit_A.Value; Right : Quotient_Unit.Value)
**return** Unit_B.Value **renames** Ops."/";
**function** "/"(Left : Unit_A.Value; Right : Unit_B.Value)
**return** Quotient_Unit.Value **renames** Ops."/";

-- Exponents all zero
Exponents : Exponent_Array **renames** Unitless.Exponents;

-- Scale_Factor of base unit is always 1.0
Scale_Factor : **constant** Scale_Type := 1.0;

-- NOTE: We do not provide the operators that combine
-- with the unitless type as they create too much ambiguity

-- Ideally this would be a generic child to avoid
-- the main generic from having a dependence on Text_IO.
-- Unfortunately, GNAT 3.14p can't deal with this as a child unit
-- so at least until that problem is remedied, Text_Output
-- is included as a nested generic rather than as a child.

-- with Ada.Units.Name_Support;
-- with Ada.Text_IO;
**generic**
**with** **package** Unit_For_Output **is** **new** Unit_Signature(<>);
Singular : **in** String := Unit_For_Output.Name;
Plural : **in** String := Name_Support.Pluralize(Singular);
-- Name for unit, plural form
Fore : Ada.Text_IO.Field := 2;
Aft : Ada.Text_IO.Field := 3; -- 3 digits after decimal pt
Exp : Ada.Text_IO.Field := 0; -- No exponent by default
**package** Text_Output **is** -- Ada.Units.System_Of_Units.Text_Output is
-- Instantiate this for text output (and Image) with units included
-- Singular is label when Val = 1.0
-- Plural is label in other cases
-- Fore is minimum digits before decimal point
-- Aft is digits after decimal point
-- Exp is number of digits allowed for exponent
-- (as used in Float_IO.Put)

This generic package is instantiated with a "base" floating point
type to be used for all unit calculations, and the names for
the various dimensions of interest to the system of units.

Throughout System_Of_Units, a unit is represented using an instantiation
of the generic "signature" package Unit_Signature. A generic signature
package is one that is instantiated simply so it can be
used as an actual parameter for a formal package parameter.

Particular units within the system of units are defined
using the nested generic packages Unit, Scaled_Unit,
Product_Unit, Quotient_Unit, and Dimensionless_Unit.
These generic packages automatically provide appropriate
multiplicative and additive operators between the units,
which do scaling as necessary, and ensure that only meaningfuul
combinations of units are performed. They also provide
an instantiation of the Unit_Signature generic signature package
so that the newly defined unit may be used with other
generics within System_Of_Units. This signature is available
with the name "Signature," and with the abbreviation "Sig."

Additional operations may be created by instantiating the
nested generic packages Scaling and Multiplicative_Operators, though
this will generally not be necessary for most uses, as the automatically
provided operators will usually be sufficient.

Finally, there is a nested generic package Text_Output which provides
an Image function and text output that includes the name of the
unit as part of the output. Note that this package might
better be a generic child unit, but not all compilers seem
to be able to handle that yet, when combined with the use
of generic signatures.

!wording

!example

Here is a simple example of use of the System_Of_Units
generic, and its nested generics.

X : meter.value := 27.0;

Y : cm.value := 540.0;

Speed : cps.value; -- cm/sec
Interval : sec.value := 33.0; -- seconds

ada.text_io.put_line("Three kilograms = " & kg_output.image(3.0));
ada.text_io.put_line("Two and a half microsecs = " &
usec_output.image(2.5));
**end** test_units;

Here is the output produced by compiling and running this test:

27.000 meters + 540.000 cm = 3240.000 cm
If traveled in 33.000 secs then speed = 98.182 cm/sec
Three kilograms = 3.000 Kg
Two and a half microsecs = 2.500 usecs

Points to note about the output:
- Text_Output by default has 3 digits after decimal point and no

exponent. This can be overridden when instantiating it.

- Text_Output by default uses the name of the unit provided

when the unit was first created.

- For scaled, product, and quotient units, the name of the unit is by

default constructed from the name(s) of the base unit(s), using the
Name_Support routines. Examples of these automatically constructed
names are "Kg", "usec" and "cm/sec".

!discussion

As mentioned in the "problem" statement, Ada's strong numeric
type checking does not work very well for multiplicative
operators in some cases, as it always allows unit-A * unit-A => unit-A,
while disallowing unit-A * unit-B => unit-C even when
meaningful. However, the ability to make certain
operators "abstract," combined with the ability to
define new numeric types and new cross-type definitions
for the multiplicative operators can provide a solution.

Unfortunately, manually building up a set of types with appropriate
cross-type multiplicative operators is error prone and
tedious. Various approaches have been tried in the past,
but using generic "signature" packages and appropriate
pragma Assert's has not been tried in combination before,
as far as we know.

In any case, whether this approach is novel or not,
it becomes significantly more valuable if there is
one, standard way of doing it, with one
standard generic signature package, etc. This will allow
more functionality (such as periodicity, more sophisticated
input and output routines, etc.) to be layered on top in
the future, and built into other shared abstractions.

Other approaches that were considered involved special
pragmas combined with using Ada subtypes rather than
derived types, special attributes, zero-space-overhead
discriminants, etc. Essentially all of these approaches
involved significant compiler development work, and
it is unclear whether the added capability could justify
the inevitably high cost of this development.

By contrast, this approach uses features already supported
by Ada 95 compilers (albeit perhaps in ways that may stretch
the capability of some compilers ;-), and seems to provide
almost all the capability that additional language features,
pragmas, or attributes could provide.

There are a few limitations of the current proposal that
we know how to fix, but are not sure whether that is
wise at this point. First of all, numeric literals
and explicit type conversion are available for the types
associated with units. This to some degree reduces the
safety guarantees provided. If the types were made
into private types, we could make stronger guarantees.
However, you would then not be able to use the types
in existing generics that require floating point types.
That also might be considered a good thing or a bad thing.
For instance it is not clear it makes sense to instantiate
Generic_Elementary_Functions with a type representing
centimeters.

It would be possible to use private types. There are
some technical difficulties when combining private
types and generic signature instantiations (which
might become the subject of another AI), but it is
possible to work around these problems. [Basically,
you need to define the private type completely in a
subpackage, and then derive from it in the package
where the instantiation of the signature takes place.]

Another limitation is that the units are only for
floating point types. This could also be solved,
by having a more elaborate set of formal parameters
to System_Of_Units, perhaps a private type
and a bunch of operators, or an instanace of a signature for a
numeric-ish type. Probably the most interesting non-floating-point
type would be type Complex, though conceivably users of fixed-point
types or even integer types might find this to be a useful
approach to units checking. Of course, since it is just
a "normal" generic package with no magic, it can be
used a model for complex, integer, or fixed-point versions.

Another important issue is whether "pre-instantiations" should
be supplied for common systems of units (e.g. "CGS").
This might lower the entry barrier to use, and would
provide "standard" examples as models as well.

Here is an example of such a pre-instantiation, and it
should probably be considered as an explicit option
in this proposal:

!appendix

Here is a prototypical implementation of the proposed packages: -------------------- package body Ada.Units.Dimension_Exponents is function "+"(Left, Right : Exponent_Array) return Exponent_Array is -- Component-wise "+" Result : Exponent_Array; begin for I in Result'Range loop Result(I) := Left(I) + Right(I); end loop; return Result; end "+"; function "-"(Left, Right : Exponent_Array) return Exponent_Array is -- Component-wise "-" Result : Exponent_Array; begin for I in Result'Range loop Result(I) := Left(I) - Right(I); end loop; return Result; end "-"; function "-"(Right : Exponent_Array) return Exponent_Array is -- Component-wise unary "-" Result : Exponent_Array; begin for I in Result'Range loop Result(I) := - Right(I); end loop; return Result; end "-"; end Ada.Units.Dimension_Exponents; package body Ada.Units.Name_Support is -- Package of string utilities useful for generating -- names automatically function Scale_Prefix(Scale_Factor : Scale_Type) return String is -- Return appropriate prefix given scale factor. -- (e.g. "kilo" for 1000.0) -- If scale factor is not something common, then return -- Scale_Type'Image(Scale_Factor) & '*' begin if Scale_Factor = 1.0 then return ""; elsif Scale_Factor = 10.0 then return "deca"; elsif Scale_Factor = 100.0 then return "heca"; elsif Scale_Factor = 1000.0 then return "kilo"; elsif Scale_Factor = 1.0E6 then return "mega"; elsif Scale_Factor = 1.0E9 then return "giga"; elsif Scale_Factor = 1.0E12 then return "tera"; elsif Scale_Factor = 1.0E15 then return "peta"; elsif Scale_Factor = 0.1 then return "deci"; elsif Scale_Factor = 0.01 then return "centi"; elsif Scale_Factor = 0.001 then return "milli"; elsif Scale_Factor = 1.0E-6 then return "micro"; elsif Scale_Factor = 1.0E-9 then return "nano"; elsif Scale_Factor = 1.0E-12 then return "pico"; else return Scale_Type'Image(Scale_Factor) & '*'; end if; end Scale_Prefix; function Short_Scale_Prefix(Scale_Factor : Scale_Type) return String is -- Return appropriate short prefix given scale factor. -- (e.g. "K" for 1000.0) -- If scale factor is not something common, then return -- Scale_Type'Image(Scale_Factor) & '*' begin if Scale_Factor = 1.0 then return ""; elsif Scale_Factor = 10.0 then return "D"; elsif Scale_Factor = 100.0 then return "H"; elsif Scale_Factor = 1000.0 then return "K"; elsif Scale_Factor = 1.0E6 then return "M"; elsif Scale_Factor = 1.0E9 then return "G"; elsif Scale_Factor = 1.0E12 then return "T"; elsif Scale_Factor = 1.0E15 then return "P"; elsif Scale_Factor = 0.1 then return "d"; elsif Scale_Factor = 0.01 then return "c"; elsif Scale_Factor = 0.001 then return "m"; elsif Scale_Factor = 1.0E-6 then return "u"; elsif Scale_Factor = 1.0E-9 then return "n"; elsif Scale_Factor = 1.0E-12 then return "p"; else return "?"; end if; end Short_Scale_Prefix; function Prefixed_Name(Unit_Name : String; Relative_Scale : Scale_Type) return String is -- Add a prefix to name to account for scale factor -- E.g., if Relative_Scale = 1000.0, return "kilo" -- or "K" depending on length of Unit_Name ("K" if -- Unit_Name <= 3 chars in length, "kilo" otherwise) begin if Unit_Name'Length <= 3 then return Short_Scale_Prefix(Relative_Scale) & Unit_Name; else return Scale_Prefix(Relative_Scale) & Unit_Name; end if; end Prefixed_Name; function Product_Name(Unit_A, Unit_B : String) return String is -- Return default name for Product_Unit, given name -- of two units forming the product. -- Currently just returns Unit_A & '-' & Unit_B begin return Unit_A & '-' & Unit_B; end Product_Name; function Quotient_Name(Numerator, Denominator : String) return String is -- Return default name for Quotient_Unit, given name -- of numerator and denominator -- Currently just returns Numerator & '/' & Denominator begin return Numerator & '/' & Denominator; end Quotient_Name; function Is_Quotient_Name(Name : String) return Boolean is -- Return true if name consists of numerator and denominator -- with separating '/' begin for I in Name'Range loop if Name(I) = '/' then return True; end if; end loop; return False; end Is_Quotient_Name; function Numerator_Part(Quotient : String) return String is -- Return numerator part of name constructed via -- Quotient_Name begin for I in Quotient'Range loop if Quotient(I) = '/' then return Quotient(Quotient'First .. I-1); end if; end loop; return Quotient; -- Should never happen end Numerator_Part; function Denominator_Part(Quotient : String) return String is -- Return denominator part of name constructed via -- Quotient_Name begin for I in Quotient'Range loop if Quotient(I) = '/' then return Quotient(I+1 .. Quotient'Last); end if; end loop; return "??"; -- Should never happen end Denominator_Part; function Pluralize(Singular_Name : String) return String is -- Return plural form given singular form. -- In general returns Singular_Name unchanged -- if it is <= 2 characters, and adds an 's' otherwise. -- TBD: Internationalization begin if Singular_Name'Length <= 2 or else Singular_Name(Singular_Name'Last) = 's' then -- Short names generally don't add an 's' -- (e.g. "g"). return Singular_Name; elsif Is_Quotient_Name(Singular_Name) then -- If is a quotient name, then pluralize the numerator -- part only return Quotient_Name( Pluralize(Numerator_Part(Singular_Name)), Denominator_Part(Singular_Name)); else -- TBD: Internationalization return Singular_Name & 's'; end if; end Pluralize; end Ada.Units.Name_Support; with Ada.Long_Float_Text_IO; -- implementation-dependent choice with Ada.Text_IO; package body Ada.Units.System_Of_Units is use type Unitless.Value; -- Provide the operators that combine with the unitless type -- Be careful not to recurse infinitely -- No scaling is necessary function "*"(Left : Base_For_Derivation; Right : Unitless.Value) return Base_For_Derivation is begin return Base_For_Derivation(Unitless.Value(Left) * Right); end "*"; function "*"(Left : Unitless.Value; Right : Base_For_Derivation) return Base_For_Derivation is begin return Base_For_Derivation(Left * Unitless.Value(Right)); end "*"; function "/"(Left : Base_For_Derivation; Right : Unitless.Value) return Base_For_Derivation is begin return Base_For_Derivation(Unitless.Value(Left) / Right); end "/"; function "/"(Left : Base_For_Derivation; Right : Base_For_Derivation) return Unitless.Value is begin return Unitless.Value(Left) / Unitless.Value(Right); end "/"; package body Scaling is -- Instantiate this to get scaling functions -- between units representing same physical dimensions -- but with different scale factors function Scale(From : Unit_A.Value) return Unit_B.Value is begin return Unit_B.Value(Unit_A.Scale_Factor / Unit_B.Scale_Factor * Scale_Type(From)); end Scale; function Scale(From : Unit_B.Value) return Unit_A.Value is begin return Unit_A.Value(Unit_B.Scale_Factor / Unit_A.Scale_Factor * Scale_Type(From)); end Scale; end Scaling; package body Scaled_Unit is -- Define the appropriate additive operators -- NOTE: These all return Base_Unit.Value to avoid -- ambiguity in complicated expressions. use type Base_Unit.Value; function "+"(Left : Base_Unit.Value; Right : Scaled_Unit.Value) return Base_Unit.Value is begin return Left + Scale(Right); end "+"; function "+"(Left : Scaled_Unit.Value; Right : Base_Unit.Value) return Base_Unit.Value is begin return Scale(Left) + Right; end "+"; function "-"(Left : Base_Unit.Value; Right : Scaled_Unit.Value) return Base_Unit.Value is begin return Left - Scale(Right); end "-"; function "-"(Left : Scaled_Unit.Value; Right : Base_Unit.Value) return Base_Unit.Value is begin return Scale(Left) - Right; end "-"; function "-"(Right : Scaled_Unit.Value) return Base_Unit.Value is begin return - Scale(Right); end "-"; end Scaled_Unit; package body Multiplicative_Operators is -- Scaling is performed automatically. Combined_Scale_Factor : constant Scale_Type := Unit_A.Scale_Factor * Unit_B.Scale_Factor / Unit_C.Scale_Factor; Inverse_Combined_Scale_Factor : constant Scale_Type := Unit_C.Scale_Factor / (Unit_A.Scale_Factor * Unit_B.Scale_Factor); function "*"(Left : Unit_A.Value; Right : Unit_B.Value) return Unit_C.Value is begin return Unit_C.Value(Combined_Scale_Factor * Scale_Type(Left) * Scale_Type(Right)); end "*"; function "*"(Left : Unit_B.Value; Right : Unit_A.Value) return Unit_C.Value is begin return Unit_C.Value(Combined_Scale_Factor * Scale_Type(Left) * Scale_Type(Right)); end "*"; function "/"(Left : Unit_C.Value; Right : Unit_A.Value) return Unit_B.Value is begin return Unit_B.Value(Inverse_Combined_Scale_Factor * Scale_Type(Left) / Scale_Type(Right)); end "/"; function "/"(Left : Unit_C.Value; Right : Unit_B.Value) return Unit_A.Value is begin return Unit_A.Value(Inverse_Combined_Scale_Factor * Scale_Type(Left) / Scale_Type(Right)); end "/"; end Multiplicative_Operators; -- with Ada.Long_Float_Text_IO; -- implementation-dependent choice -- GNAT can't deal with this as a generic child package body Text_Output is subtype IO_Type is Long_Float; -- implementation-dependent choice package Float_IO renames Ada.Long_Float_Text_IO; -- implementation-dependent choice function Name(Is_One : Boolean) return String is -- Return Singular or Plural, depending on -- whether Val = 1.0 (i.e. Is_One true) begin if Is_One then return Singular; else return Plural; end if; end Name; procedure Put(Val : in Value) is begin Float_IO.Put(IO_Type(Val), Fore => Fore, Aft => Aft, Exp => Exp); Ada.Text_IO.Put(' ' & Name(Is_One => Val = 1.0)); end Put; procedure Put(File : in Ada.Text_IO.File_Type; Val : in Value) is begin Float_IO.Put(File, IO_Type(Val), Fore => Fore, Aft => Aft, Exp => Exp); Ada.Text_IO.Put(File, ' ' & Name(Is_One => Val = 1.0)); end Put; function Image(Val : in Value) return String is Extra_Digits_Before_Decimal : constant := 30; -- This is extra room allowed in the Result string -- declared below. -- TBD: The value "30" allows for the case when EXP -- is zero. The value 30 is pretty arbitrary but -- should be more than sufficient. Result : String(1 .. Fore + Aft + Exp + 2 + Extra_Digits_Before_Decimal); begin Float_IO.Put(Result, IO_Type(Val), Aft => Aft, Exp => Exp); for I in Result'Range loop if Result(I) /= ' ' then return Result(I..Result'Last) & ' ' & Name(Is_One => Val = 1.0); end if; end loop; return "??"; -- Should never happen end Image; end Text_Output; end Ada.Units.System_Of_Units; !ACATS test ACATS tests are needed to test this package throughly. !appendix From: Pascal Leroy Sent: Monday, February 3, 2003 9:14 AM I like it. Random comments: I wonder why you pass Names_Of_Dimensions and not an instantiation of Dimension_Exponents to System_Of_Units. If you work with several system of units, it would seem that factoring the exponents could be useful. (I am told that there are countries were SI is not the only system of units in practical use.) I think this should not be restricted to floating-point types. Fixed-point types and complex come to mind. References to GNAT bugs have nothing to do in an AI, unless we want to enshrine them in the RM ;-) The style of Text_Output, where you pass Fore/Aft/Exp at instantiation time, is different from that of, say, Complex_IO. I don't think that's a good idea. A preinstantiation should be provided for the SI system. Btw, some of the examples will need to be sanitized to be in line with the SI conventions (s instead of sec, K is for Kelvin, k is for kilo, etc.). Incidentally, there is in Standard.Character a Greek mu (position 181) precisely to be able to write SI prefixes. Would there be a way to provide conversion of units? For instance you would say that an inch is 2.54 cm and it would know how to convert square inches to square centimeters. That would be cool, but I don't know if it's possible. **************************************************************** From: Tucker Taft Sent: Monday, February 3, 2003 1:26 PM Pascal Leroy wrote: > > > !subject Physical Units Checking > > I like it. > > Random comments: > > I wonder why you pass Names_Of_Dimensions and not an instantiation of > Dimension_Exponents to System_Of_Units. If you work with several system of > units, it would seem that factoring the exponents could be useful. (I am told > that there are countries were SI is not the only system of units in practical > use.) The Dimension_Exponents package was really just to simplify the visible part of System_Of_Units, so it didn't include the operators for Exponent_Arrays. I played around with using a formal package parameter instead of the enumeration type for the names of dimensions, but it seemed to add complexity for the user without any obvious additional functionality. You can still factor out the exponents by having multiple instantiations of System_Of_Units all use the same Names_Of_Dimensions type. If some other abstraction really wanted a formal package instead of the formal type, the instantiator has an instance of Dimension_Exponents called "Dimensions" available in any instantiation of System_Of_Units. > I think this should not be restricted to floating-point types. Fixed-point > types and complex come to mind. I discussed this in the AI. I fear it may make the interface a bit daunting. Perhaps if we provide pre-instantiations, that becomes less of an issue. > > References to GNAT bugs have nothing to do in an AI, unless we want to enshrine > them in the RM ;-) I agree with that, but we presumably don't want to propose something that can't be compiled by popular compilers, so I wanted to explain why I made Text_Output a nested package. That could presumably be relegated to the discussion section only, or the ACT folks could sign up to fix the problem real-soon-now. > > The style of Text_Output, where you pass Fore/Aft/Exp at instantiation time, is > different from that of, say, Complex_IO. I don't think that's a good idea. I suppose consistency make sense. But having the Default_Fore, etc. as global variables just seems gratuitously non thread-safe. Perhaps the generic parameters could be the Defaults, and then each Put function (and Image) could take Fore/Aft/Exp as actual parameters. Or perhaps I could just accept the gnarly "standard" approach to Default_Fore/Aft/Exp and get on with it... ;-). I guess I still think the default ought to be without an exponent. The output is so much easier to read without exponents flying around, and one of the points of units is so you don't have to keep talking about 3.5E-9 meters, and instead can talk about 3.5 nm. > A preinstantiation should be provided for the SI system. Btw, some of the > examples will need to be sanitized to be in line with the SI conventions (s > instead of sec, K is for Kelvin, k is for kilo, etc.). Incidentally, there is > in Standard.Character a Greek mu (position 181) precisely to be able to write SI > prefixes. I will need some help with the SI preinstantiation. Maybe Thom Brooke can help do that. I am definitely not an expert in SI. > Would there be a way to provide conversion of units? For instance you would say > that an inch is 2.54 cm and it would know how to convert square inches to square > centimeters. That would be cool, but I don't know if it's possible. That is already possible. Any two units that have the same dimensions can have a scaling function created for them by instantiating the Scaling generic. So if you build up square-cm and square-inch using Product_Unit from cm and inch, and inch is defined in terms of cm using Scaled_Unit (or vice-versa), then Scaling can be instantiated with square-cm and square-inch and the Scale functions will do the right thing. You don't have to explicitly instantiate Scaling if the units are defined directly in terms of each other using Scaled_Unit (since it pre-instantiates Scaling), but if the units are only indirectly related, the Scaling generic may be used to produce direct Scale converters. See the attached "test_squares.ada" example. Here is the output of running the attached example to get this kind of scaling: 30.000 square-cms converts to 4.650 square-inches ---- with Ada.Text_IO; with Ada.Units.system_of_units; with Ada.Units.System_Of_Units.Text_Output; use Ada.Units; procedure test_squares is type dim_names is (length, mass, time); package cgs is new system_of_units(dim_names, Standard.Float); use type cgs.unitless.value; package cm is new cgs.unit("cm", (length => 1, others => 0)); package cm_output is new cgs.text_output(cm.signature); package square_cm is new cgs.product_unit(cm.sig, cm.sig, name => "square-cm"); package sqcm_output is new cgs.text_output(square_cm.sig); package inch is new cgs.scaled_unit(cm.sig, relative_scale => 2.54, name => "inch"); package square_inch is new cgs.product_unit(inch.sig, inch.sig, name => "square-inch"); package sqin_output is new cgs.text_output(square_inch.sig, plural => "square-inches"); package square_scaling is new cgs.scaling( square_cm.sig, square_inch.sig); sqcm : square_cm.value := 30.0; sqi : square_inch.value := square_scaling.scale(sqcm); begin Ada.Text_IO.Put_Line(sqcm_output.image(sqcm) & " converts to " & sqin_output.image(sqi)); end; **************************************************************** From: Robert I. Eachus Sent: Monday, February 3, 2003 1:47 PM I need to do more reading of the proposal, but there is one comment I would like to make up front. There are some computations where you run into non-integer values for units. These have no meaning as such in the final results, but are needed in intermediates. I never tried to deal with exponentials and logs, but half-integer powers show up all over the place. The "trick" I used to get around this was to use Paul Hilfinger's package with all the units multiplied by two. That way the 3/2 power of length had an exponent of 3. I recently commented to Ben Brogol on this that using a multiplier of 6 might be worthwhile, or even 12. Sixty, which allows for square, cube, fourth and fifth roots looks very execessive. But doing it this way allowed for a meaningful square root function for values with units. Of course, it would be nicest if the exponent values could be hiden from sight. In other words: type Meter is private; but then you lose the visible numeric literals. Of course that is not necessarily a bad thing: X: Centimeters := 1.0 * Cm; **************************************************************** From: Tucker Taft Sent: Monday, February 3, 2003 2:21 PM My initial version of this used floats for the exponents, and that worked fine (they aren't discriminants, so there is really no problem supporting floats as exponents). My co-sponsor didn't think it was necessary to accommodate non-integral exponents, so I switched to integers. But it could easily be switched back. Of course we would then probably want a version of Sqrt that would do the "right thing" with respect to units, which is quite possible. Also, a Square_Root_Unit generic would be desirable. Note that exponents are "fiddled with" only at generic instantiation time, so overhead relating to using floating point exponents would be irrelevant. > Of course, it would be nicest if the exponent values could be hiden from > sight. In other words: > > type Meter is private; The visibility of the exponents has *nothing* to do with the privateness of the type in this proposal. You probably need to take another look at it... > but then you lose the visible numeric literals. Of course that is not > necessarily a bad thing: > > X: Centimeters := 1.0 * Cm; I discussed the issue of privateness in the AI. More specific comments on this topic are welcome. **************************************************************** From: Robert A. Duff Sent: Monday, February 3, 2003 2:42 PM > My initial version of this used floats for the exponents, and that > worked fine (they aren't discriminants, so there is really no > problem supporting floats as exponents). I would think the exponents should be fixed point. If you want integer exponents, you use 'Small=1.0. If you want square roots, you use 'Small=0.5. Or whatever else you want. **************************************************************** From: Gary Dismukes Sent: Monday, February 3, 2003 2:14 PM As Pascal pointed out: > > References to GNAT bugs have nothing to do in an AI, unless we want to enshrine > them in the RM ;-) Yes, please make sure that the two references to GNAT in the nested generic package Text_Output (both in the spec and the later sample body) are excised in the first version of this AI that's officially posted to the web site. -- Gary (on behalf of Ada Core Technologies:) **************************************************************** From: Tucker Taft Sent: Monday, February 3, 2003 3:29 PM Well, at least I got your attention ;-). **************************************************************** From: Gary Dismukes Sent: Monday, February 3, 2003 4:10 PM Probably the best thing to do in a case like this is to send a problem report to report@gnat.com with a complete test case. It seems reasonable to assume that in a case of legal Ada like this that compilers will (and should) get fixed to allow it. Anyway, at least now we know about it. ;-) **************************************************************** From: Pascal Leroy Sent: Tuesday, February 4, 2003 3:08 AM > > The style of Text_Output, where you pass Fore/Aft/Exp at instantiation > > time, is different from that of, say, Complex_IO. I don't think that's > > a good idea. > > I suppose consistency make sense. But having the Default_Fore, etc. as > global variables just seems gratuitously non thread-safe. Perhaps the > generic parameters could be the Defaults, and then each Put function > (and Image) could take Fore/Aft/Exp as actual parameters. Or perhaps > I could just accept the gnarly "standard" approach to Default_Fore/Aft/Exp > and get on with it... ;-). I guess I still think the default ought > to be without an exponent. The output is so much easier to read without > exponents flying around, and one of the points of units is so you don't > have to keep talking about 3.5E-9 meters, and instead can talk about 3.5 nm. Fine. What I am suggesting is to use the style of Text_IO, but use 0 for the default exponent. That's exactly what Text_IO.Decimal_IO does, actually (I suppose that accountants are not too happy with numbers like 1.35E5 USD). > I will need some help with the SI preinstantiation. Maybe Thom Brooke > can help do that. I am definitely not an expert in SI. I am not an expert either, but having had some scientific education in a country where SI is a religious dogma I know the basics. I think I can help with the preinstantiation. > Here is the output of running the attached example to get this kind of scaling: > > 30.000 square-cms converts to 4.650 square-inches Great. **************************************************************** From: Christoph Grein Sent: Tuesday, February 4, 2003 4:00 AM !subject Physical Units Checking Jean-Pierre Rosen sent me this new AI to comment because I've prepared a paper for Ada-Europe 2003 giving a survey of physical units handling techniques in Ada. While the proposal includes a lot of clever ideas about using generics with (generic) signature packages, I must say that on first sight, I'm not very impressed by the outcome. The proposed method is aimed at compile-time checking of dimensions, but it suffers like all the members of this family from the combinatorial explosion of instantiations and overloaded operators. And despite all these overloadings, it remains silent how equations like d = (1/2) g t^2 should be handled when solved for t, because powers and roots are not included. This may sound like a very harsh wording, and I will put some flesh around the bones of my critique presently. Please do not misunderstand me. I do not want to say that there are no merits in this proposal. I only think it is not a good idea to standardise it and say: "Look, this is the way you have to deal in Ada with dimensions." Let me rephrase Robert Dewar, who, referring to proposals for standard container libraries, said: Put the proposal on the net, make it public, and wait for the reaction. If the proposal finds support, you can later think about standardisation. Let me start with nit-picking and then go on to the more severe deficiencies as I see them. 1. Names Names of units (especially SI) are case sensitive. The currently defined prefixes are the following: yocto: constant := 1.0E-24; -- y zepto: constant := 1.0E-21; -- z atto : constant := 1.0E-18; -- a femto: constant := 1.0E-15; -- f pico : constant := 1.0E-12; -- p nano : constant := 1.0E-09; -- n micro: constant := 1.0E-06; -- æ (u) milli: constant := 1.0E-03; -- m centi: constant := 1.0E-02; -- c deci : constant := 1.0E-01; -- d deka : constant := 1.0E+01; -- da hecto: constant := 1.0E+02; -- h kilo : constant := 1.0E+03; -- k mega : constant := 1.0E+06; -- M giga : constant := 1.0E+09; -- G tera : constant := 1.0E+12; -- T peta : constant := 1.0E+15; -- P exa : constant := 1.0E+18; -- E zetta: constant := 1.0E+21; -- Z yotta: constant := 1.0E+24; -- Y It further uses the fancy name "heca" for "hecto" and gets some prefix symbols wrong: "deca" is 'da' not 'D' "hecto" is 'h' not 'H' "kilo" is 'k' not 'K' Also the symbol for Second is 's', not "sec". Speed is measured in "m/s" not "m/sec". See the National Institute of Standards and Technology http://physics.nist.gov/cuu/Units/ This of course is nit-picking and can easily be cured. 2. Product Unit Name Why is the default name of a product unit Unit_a & '-' & Unit_B? Is this a misprint? Unit_a & '*' & Unit_B seems more appropriate and is in line with the quotient name Unit_a & '/' & Unit_B. J = N-m vs. J = N*m 3. Mathematical Functions These are not included. There is a statement: "For instance it is not clear it makes sense to instantiate Generic_Elementary_Functions with a type representing centimeters." It is absolutely clear that it does not at all make sense. exp (5 cm), ln (5 cm), sin (5 cm) all are nonesense. However sqrt (5 cm) makes perfectly sense as does sin (5 cm, cycle => 10 cm) and tan (y => 10 cm, x => 5 cm). So mathematics has to be included up front, not added later. 4. Exponentiation and Roots This is the most prominent shortcoming. How do you want to deal with the most simple mechanical equation cited above d = (1/2) g t^2 let along with the Stefan Boltzmann equation S = Sigma * T^4 [S] = W/m^2, or the Schottky-Langmuir equation j = (4/9) eps0 Sqrt (2 e0 / m0) U^(3/2) / d^2 [j] = A/m^2? When you solve these equations for t, T, U, you need roots. And Schottky-Langmuir shows that you even need fractional powers. While in SI, final results never have fractional powers, intermediate results do, and you want to be able to rearrange items in equations as is most appropriate to the problem at hand and do not want to be forced to write just the way the method supports. U^(3/2) => Sqrt (U * U * U) -- what a mess j^(2/3) => ??? 5. Combinatorial Explosion As the examples above show, this method suffers severly from the combinatorial explosion of operations needed. Up to which power do you want to go? At least the fourth power seems reasonable and for the inverse you then need square roots, cubic roots, fourth roots. If you do 3D vector dynamics, calculation of the absolute value of an acceleration vector needs time in the (negative) fourth power. Physics with all its powers and roots evades these (I'm apt to say: naive) attempts. The example code uses just three dimensions, Length, Time, Mass. In such a restricted world, the proposal might work (but see below), but I doubt that you can make it work for all seven SI base units. 6. Unit Systems As an example, the CGS system is used, however only in a very restricted form based on just cm, g, s. If you want to include electricity, you immediately run into severe problems with this proposal. The most appropriate CSG system is the Gaussian one (outdated as any other CSG from practical use): It is symmetric in electric and magnetic effects, i.e. Maxwell's equations are symmetric in vacuum and corresponding electric and magnetic items have the same dimensions (albeit carrying different names); there is just one fundamental constant, the speed of light. The problem is that the basic unit for electric charge has fractional powers: 1 esu = 1 cm^(3/2) g^(1/2) s^(-1) How are you going to handle this? 7. Clumsiness Basing everything on a set of generics is appealing from a theoretic point of view, but if I, as a developer, am given this set, it seems very awkward for me to set up the system with the dimensions I need. What developers want, is a simple set of ready-to-use packages. Until I see an example with more than just three dimensions (best would be the full SI system) where I can write U := cuberoot (Whatever); -- [U] = V as for Schottky-Langmuir above, without being artificially forced to arrange items in Whatever in a form the current instance can handle, I'm not convinced that this proposal is ripe for standardisation. 8. Abstract Operators An annoying feature is that illegal operators like time * time -> time are present and made abstract, rather than being absent. Abstractness does not prevent operators to be taken into account upon overload resolution. So they may raise their ugly heads every now and then to the dismay of the poor user of the proposal (see below). 9. Over-Ambitiousness While being severely flawed in some aspects, the proposal is over-ambitious in wanting to provide natural language names for the units. 27.000 meters + 540.000 cm = 3240.000 cm Expecting such an output is silly. What one _can_ expect is 27.000 m + 540.000 cm = 3240.000 cm Natural language output like the following is out of the due purpose of a package for unit handling: If traveled in 33.000 secs then speed = 98.182 cm/sec In Italian, kilogram is chilogramm, you buy "un etto" or "due etti" of something, i.e. 1 or 2 hg (hectogram, not Hg, "Hecagramm" - Hg is quicksilver). Do you really want to go into this business of internationalising plurals? 10. Example I have added an equation that is quite common: M = m0 * (T/Tau)^2 and written it in several forms. The result is below. Many of the failing statements can be cured be adding yet more instantiations. This is what I mean with combinatorial explosion. Even for only three dimensions, this is becoming awkward. And if you write it in the most natural way, abstract operations get into your way. use type kg.value, sec.value; T, Tau: sec.value := 1.0; M, m0 : kg .value := 1.0; begin M := m0 * (T*T/(Tau*Tau)); -- 1 2 -- >>> cannot call abstract subprogram ""*"" -- >>> cannot call abstract subprogram ""*"" M := m0 * (T*T)/(Tau*Tau); -- 1 4 -- >>> invalid operand types for operator "*" -- >>> left operand has type "Ada.Units.System_Of_Units.Value" from instance at line 28 -- >>> right operand has type "Ada.Units.System_Of_Units.Value" from instance at line 19 -- >>> expected type "Ada.Units.System_Of_Units.Value" from instance at line 28 -- >>> found type "Ada.Units.System_Of_Units.Value" from instance at line 19 M := m0 * T*T/(Tau*Tau); -- 1 4 6 -- >>> invalid operand types for operator "*" -- >>> left operand has type "Ada.Units.System_Of_Units.Value" from instance at line 28 -- >>> right operand has type "Ada.Units.System_Of_Units.Value" from instance at line 19 -- >>> expected type "Ada.Units.System_Of_Units.Value" from instance at line 28 -- >>> found type "Ada.Units.System_Of_Units.Value" from instance at line 19 -- >>> expected type "Ada.Units.System_Of_Units.Value" from instance at line 28 -- >>> found type "Ada.Units.System_Of_Units.Value" from instance at line 19 M := m0 * (T/Tau)*(T/Tau); M := m0 * (T/Tau*T/Tau); -- | -- >>> ambiguous expression (cannot resolve ""*"") -- >>> possible interpretation at a-usofun.ads:94, instance at line 19 M := m0 * T/Tau*T/Tau; -- 1 4 6 8 -- >>> invalid operand types for operator "*" -- >>> left operand has type "Ada.Units.System_Of_Units.Value" from instance at line 28 -- >>> right operand has type "Ada.Units.System_Of_Units.Value" from instance at line 19 -- >>> expected type "Ada.Units.System_Of_Units.Value" from instance at line 28 -- >>> found type "Ada.Units.System_Of_Units.Value" from instance at line 19 -- >>> expected type "Ada.Units.System_Of_Units.Value" from instance at line 28 -- >>> found type "Ada.Units.System_Of_Units.Value" from instance at line 19 -- >>> expected type "Ada.Units.System_Of_Units.Value" from instance at line 28 -- >>> found type "Ada.Units.System_Of_Units.Value" from instance at line 19 M := m0 * T**2/Tau**2; -- 1 4 -- >>> invalid operand types for operator "*" -- >>> left operand has type "Ada.Units.System_Of_Units.Value" from instance at line 28 -- >>> right operand has type "Ada.Units.System_Of_Units.Value" from instance at line 19 -- >>> expected type "Ada.Units.System_Of_Units.Value" from instance at line 28 -- >>> found type "Ada.Units.System_Of_Units.Value" from instance at line 19 M := m0 * (T/Tau)**2; end test_units; -------------------------------------------------------------------------- Please let me summarize. I do _not_ say that the method does not have its merits. I only want to point out that it is, in its present form, not fit to handle physics in its whole generality - which would be implied if it were standardised. And, by the way, it wasn't the wrong equations that made Mars Lander fail, it was improper mixing of imperial and metric units in communicating CSCIs, and this method, as any other method, would not have prevented this failure, since units are not transferred via data buses nor global memory. Until shown a convincing example of a compile-time solution, I do believe that full dimension handling can only be done during run-time (in Ada as it is now). C++ with its implicit instantiation of templates can do during compile-time what Ada can do only during run-time. And indeed, there was a proposal of such a feature for Ada9X: J. Shen, G. V. Cormack, Automatic Instantiation in Ada, ACM 0-89791-445-7/91/1000-0338 $1.50 (I do not say that I think inclusion of implicit instantiation into Ada is a sensible thing. I let decide upon this by more competent persons.) **************************************************************** From: Pascal Leroy Sent: Tuesday, February 4, 2003 6:39 AM > 8. Abstract Operators > > An annoying feature is that illegal operators like time * time -> time are > present and made abstract, rather than being absent. > > Abstractness does not prevent operators to be taken into account upon > overload resolution. So they may raise their ugly heads every now and then > to the dismay of the poor user of the proposal (see below). Note that AI 310 proposes a mechanism to ignore abstract operations during overload resolutions in some cases. This would help a lot in the context of this AI. In fact, I don't believe that an appropriate static solution to the issue of physical unit checking can be found without AI 310 (or something similar). **************************************************************** From: David C. Hoos, Sr. Sent: Tuesday, February 4, 2003 7:10 AM > However sqrt (5 cm) makes perfectly sense as does sin (5 cm, cycle => 10 cm) > and tan (y => 10 cm, x => 5 cm). Did you mean arctan (y => 10 cm, x => 5 cm)? **************************************************************** From: Christoph Grein Sent: Tuesday, February 4, 2003 7:22 AM Yes, of course. Thanx for reading so well. **************************************************************** From: Pascal Leroy Sent: Tuesday, February 4, 2003 7:11 AM > 2. Product Unit Name > > Why is the default name of a product unit Unit_a & '-' & Unit_B? > Is this a misprint? Unit_a & '*' & Unit_B seems more appropriate and > is in line with the quotient name Unit_a & '/' & Unit_B. > > J = N-m vs. J = N*m Actually, the character at position 183 is a middle dot which appears to be the recommended way to express products of units (see http://www.bipm.fr/pdf/si-brochure.pdf, paragraph 5.3). **************************************************************** From: Tucker Taft Sent: Tuesday, February 4, 2003 3:57 PM Pascal Leroy wrote: > > > 8. Abstract Operators > > > > An annoying feature is that illegal operators like time * time -> time are > > present and made abstract, rather than being absent. > > > > Abstractness does not prevent operators to be taken into account upon > > overload resolution. So they may raise their ugly heads every now and then > > to the dismay of the poor user of the proposal (see below). > > Note that AI 310 proposes a mechanism to ignore abstract operations during > overload resolutions in some cases. This would help a lot in the context of > this AI. In fact, I don't believe that an appropriate static solution to the > issue of physical unit checking can be found without AI 310 (or something > similar). Actually, I think my attempt to use visible floating point types is doomed, so I am in the process of changing to using private types except for unitless/dimensionless types. This will eliminate the need for making any predefined operators abstract. The reason why using visible floating point types is doomed is that numeric literals and named numbers will be usable anywhere and everywhere, either creating lots of ambiguity, or "nicely" allowing the user to violate the units checking model they so carefully constructed. So my plan is to add "Val" and "Mag" (for "Magnitude") functions as follows: function Val(Mag : Unitless.Value) return My_Unit.Value; function Mag(Val : My_Unit.Value) return Unitless.Value; These are analogous to the 'Val and 'Pos attributes of enumeration types. To create a literal of a given unit, you can write "My_Unit.Val(3.325)". If for some reason you want the underlying magnitude of a "My_Unit" value, you can do something like use "My_Unit.Mag(X)". Of course, the operators defined by the various _Unit packages will use the "Mag()" and "Val()" functions extensively to convert to/from a "real" floating point type. So, stay tuned for the next version... ****************************************************************

Questions? Ask the ACAA Technical Agent