Skip to content

Systems of Quantities

Most physical units libraries focus on modeling one or more systems of units. However an equally (or more) important abstraction is the system of quantities.

Info

mp-units is likely the first Open Source library (in any language) that models the ISQ with the full ISO 80000 definition set. Feedback is welcome.

Dimension is not enough to describe a quantity

Most libraries understand dimensions, yet a dimension alone does not fully describe a quantity. Consider:

class Box {
  area base_;
  length height_;
public:
  Box(length l, length w, length h) : base_(l * w), height_(h) {}
  // ...
};

Box my_box(2 * m, 3 * m, 1 * m);

This interface is ambiguous. Many strongly typed libraries cannot do better 🥴

Another common question: how to differentiate work and torque? They share a dimension yet differ semantically.

A similar issue is related to figuring out what should be the result of:

auto res = 1 * Hz + 1 * Bq + 1 * Bd;

where:

  • Hz (hertz) - unit of frequency
  • Bq (becquerel) - unit of activity
  • Bd (baud) - unit of modulation rate

All have the same dimension \(\mathsf{T}^{-1}\), but adding or comparing them is meaningless.

Consider fuel consumption (fuel volume divided by distance, e.g. 6.7 l/km) vs an area. Both have dimension \(\mathsf{L}^{2}\) yet adding them is nonsensical and should fail.

Important

More than one quantity may be defined for the same dimension:

  • quantities of different kinds (e.g. frequency, modulation rate, activity, ...)
  • quantities of the same kind (e.g. length, width, altitude, distance, radius, wavelength, position vector, ...)

These issues require proper modeling of a system of quantities.

Quantities of the same kind

ISO 80000-1

  • Quantities may be grouped together into categories of quantities that are mutually comparable
  • Mutually comparable quantities are called quantities of the same kind
  • Two or more quantities cannot be added or subtracted unless they belong to the same category of mutually comparable quantities
  • Quantities of the same kind within a given system of quantities have the same quantity dimension
  • Quantities of the same dimension are not necessarily of the same kind

ISO 80000 answers the earlier questions: two quantities cannot be added, subtracted, or compared unless they are of the same kind. Thus frequency, activity, and modulation rate are incompatible.

System of quantities is not only about kinds

ISO 80000 specifies hundreds of quantities in many kinds; kinds often contain multiple quantities forming a hierarchy.

For example, here are all quantities of the kind length provided in the ISO 80000:

flowchart TD
    length["<b>length</b><br>[m]"]
    length --- width["<b>width</b> / <b>breadth</b>"]
    length --- height["<b>height</b> / <b>depth</b> / <b>altitude</b>"]
    width --- thickness["<b>thickness</b>"]
    width --- diameter["<b>diameter</b>"]
    width --- radius["<b>radius</b>"]
    length --- path_length["<b>path_length</b>"]
    path_length --- distance["<b>distance</b>"]
    distance --- radial_distance["<b>radial_distance</b>"]
    length --- wavelength["<b>wavelength</b>"]
    length --- displacement["<b>displacement</b><br>{vector}"]
    displacement --- position_vector["<b>position_vector</b>"]
    radius --- radius_of_curvature["<b>radius_of_curvature</b>"]

Each quantity above expresses some kind of length and can be measured with si::metre. Each has different semantics and sometimes a distinct representation (e.g. position_vector and displacement are vector quantities).

The hierarchy guides valid arithmetic and conversion rules for quantities of the same kind.

Defining quantities

All quantity information resides in quantity_spec. To define a quantity inherit a strong type from a suitable instantiation.

Tip

Quantity specification definitions benefit from an explicit object parameter added in C++23 to remove the need for CRTP idiom, which significantly simplifies the code. However, as C++23 is far from being mainstream today, a portability macro QUANTITY_SPEC() is provided and used consistently through the library to allow the code to compile with C++20 compilers, thanks to the CRTP usage under the hood.

See more in the C++ compiler support chapter.

For example, here is how the above quantity kind tree can be modeled in the library:

inline constexpr struct length final : quantity_spec<dim_length> {} length;
inline constexpr struct width final : quantity_spec<length> {} width;
inline constexpr auto breadth = width;
inline constexpr struct height final : quantity_spec<length> {} height;
inline constexpr auto depth = height;
inline constexpr auto altitude = height;
inline constexpr struct thickness final : quantity_spec<width> {} thickness;
inline constexpr struct diameter final : quantity_spec<width> {} diameter;
inline constexpr struct radius final : quantity_spec<width> {} radius;
inline constexpr struct radius_of_curvature final : quantity_spec<radius> {} radius_of_curvature;
inline constexpr struct path_length final : quantity_spec<length> {} path_length;
inline constexpr auto arc_length = path_length;
inline constexpr struct distance final : quantity_spec<path_length> {} distance;
inline constexpr struct radial_distance final : quantity_spec<distance> {} radial_distance;
inline constexpr struct wavelength final : quantity_spec<length> {} wavelength;
inline constexpr struct displacement final : quantity_spec<length, quantity_character::vector> {} displacement;
inline constexpr struct position_vector final : quantity_spec<displacement> {} position_vector;
inline constexpr struct length final : quantity_spec<length, dim_length> {} length;
inline constexpr struct width final : quantity_spec<width, length> {} width;
inline constexpr auto breadth = width;
inline constexpr struct height final : quantity_spec<height, length> {} height;
inline constexpr auto depth = height;
inline constexpr auto altitude = height;
inline constexpr struct thickness final : quantity_spec<thickness, width> {} thickness;
inline constexpr struct diameter final : quantity_spec<diameter, width> {} diameter;
inline constexpr struct radius final : quantity_spec<radius, width> {} radius;
inline constexpr struct radius_of_curvature final : quantity_spec<radius_of_curvature, radius> {} radius_of_curvature;
inline constexpr struct path_length final : quantity_spec<path_length, length> {} path_length;
inline constexpr auto arc_length = path_length;
inline constexpr struct distance final : quantity_spec<distance, path_length> {} distance;
inline constexpr struct radial_distance final : quantity_spec<radial_distance, distance> {} radial_distance;
inline constexpr struct wavelength final : quantity_spec<wavelength, length> {} wavelength;
inline constexpr struct displacement final : quantity_spec<displacement, length, quantity_character::vector> {} displacement;
inline constexpr struct position_vector final : quantity_spec<position_vector, displacement> {} position_vector;
QUANTITY_SPEC(length, dim_length);
QUANTITY_SPEC(width, length);
inline constexpr auto breadth = width;
QUANTITY_SPEC(height, length);
inline constexpr auto depth = height;
inline constexpr auto altitude = height;
QUANTITY_SPEC(thickness, width);
QUANTITY_SPEC(diameter, width);
QUANTITY_SPEC(radius, width);
QUANTITY_SPEC(radius_of_curvature, radius);
QUANTITY_SPEC(path_length, length);
inline constexpr auto arc_length = path_length;
QUANTITY_SPEC(distance, path_length);
QUANTITY_SPEC(radial_distance, distance);
QUANTITY_SPEC(wavelength, length);
QUANTITY_SPEC(displacement, length, quantity_character::vector);
QUANTITY_SPEC(position_vector, displacement);

Note

More information on how to define a system of quantities can be found in the "International System of Quantities (ISQ)" chapter.

Comparing, adding, and subtracting quantities

ISO 80000 states that width and height are quantities of the same kind; therefore they:

  • are mutually comparable,
  • can be added and subtracted.

If we take the above for granted, the only reasonable result of 1 * width + 1 * height is 2 * length, where the result of length is known as a common quantity type. A result of such an equation is always the first common node in a hierarchy tree of the same kind. For example:

static_assert(get_common_quantity_spec(isq::width, isq::height) == isq::length);
static_assert(get_common_quantity_spec(isq::thickness, isq::radius) == isq::width);
static_assert(get_common_quantity_spec(isq::distance, isq::path_length) == isq::path_length);

Converting between quantities

Based on the same hierarchy of quantities of kind length, we can define quantity conversion rules.

  1. Implicit conversions

    • every width is a length
    • every radius is a width
    static_assert(implicitly_convertible(isq::width, isq::length));
    static_assert(implicitly_convertible(isq::radius, isq::width));
    static_assert(implicitly_convertible(isq::radius, isq::length));
    

    Implicit conversions are allowed on copy-initialization:

    void foo(quantity<isq::length[m]> q);
    
    quantity<isq::width[m]> q1 = 42 * m;
    quantity<isq::length[m]> q2 = q1;  // implicit quantity conversion
    foo(q1);                           // implicit quantity conversion
    
  2. Explicit conversions

    • not every length is a width
    • not every width is a radius
    static_assert(!implicitly_convertible(isq::length, isq::width));
    static_assert(!implicitly_convertible(isq::width, isq::radius));
    static_assert(!implicitly_convertible(isq::length, isq::radius));
    static_assert(explicitly_convertible(isq::length, isq::width));
    static_assert(explicitly_convertible(isq::width, isq::radius));
    static_assert(explicitly_convertible(isq::length, isq::radius));
    

    Explicit conversions are forced by passing the quantity to a call operator of a quantity_spec type or by calling quantity's explicit constructor:

    void foo(quantity<isq::height[m]> q);
    
    quantity<isq::length[m]> q1 = 42 * m;
    quantity<isq::height[m]> q2 = isq::height(q1);  // explicit quantity conversion
    quantity<isq::height[m]> q3(q1);                // direct initialization
    foo(isq::height(q1));                           // explicit quantity conversion
    
  3. Explicit casts

    • height is not a width
    • both height and width are quantities of kind length
    static_assert(!implicitly_convertible(isq::height, isq::width));
    static_assert(!explicitly_convertible(isq::height, isq::width));
    static_assert(castable(isq::height, isq::width));
    

    Explicit casts are forced with a dedicated quantity_cast function:

    void foo(quantity<isq::height[m]> q);
    
    quantity<isq::width[m]> q1 = 42 * m;
    quantity<isq::height[m]> q2 = quantity_cast<isq::height>(q1);  // explicit quantity cast
    foo(quantity_cast<isq::height>(q1));                           // explicit quantity cast
    
  4. No conversion

    • time has nothing in common with length
    static_assert(!implicitly_convertible(isq::time, isq::length));
    static_assert(!explicitly_convertible(isq::time, isq::length));
    static_assert(!castable(isq::time, isq::length));
    

    Even the explicit casts will not force such a conversion:

    void foo(quantity<isq::length[m]>);
    
    quantity<isq::length[m]> q1 = 42 * s;    // Compile-time error
    foo(quantity_cast<isq::length>(42 * s)); // Compile-time error
    

Hierarchies of derived quantities

Derived quantity equations often do not automatically form a hierarchy tree. This is why it is sometimes not obvious what such a tree should look like. Also, ISO explicitly states:

ISO/IEC Guide 99

The division of ‘quantity’ according to ‘kind of quantity’ is, to some extent, arbitrary.

The below presents some arbitrary hierarchy of derived quantities of kind energy:

flowchart TD
    energy["<b>energy</b><br><i>(mass * length<sup>2</sup> / time<sup>2</sup>)</i><br>[J]"]
    energy --- mechanical_energy["<b>mechanical_energy</b>"]
    mechanical_energy --- potential_energy["<b>potential_energy</b>"]
    potential_energy --- gravitational_potential_energy["<b>gravitational_potential_energy</b><br><i>(mass * acceleration_of_free_fall * height)</i>"]
    potential_energy --- elastic_potential_energy["<b>elastic_potential_energy</b><br><i>(spring_constant * amount_of_compression<sup>2</sup>)</i>"]
    mechanical_energy --- kinetic_energy["<b>kinetic_energy</b><br><i>(mass * speed<sup>2</sup>)</i>"]
    energy --- enthalpy["<b>enthalpy</b>"]
    enthalpy --- internal_energy["<b>internal_energy</b> / <b>thermodynamic_energy</b>"]
    internal_energy --- Helmholtz_energy["<b>Helmholtz_energy</b> / <b>Helmholtz_function</b>"]
    enthalpy --- Gibbs_energy["<b>Gibbs_energy</b> / <b>Gibbs_function</b>"]
    energy --- active_energy["<b>active_energy</b>"]

Notice, that even though all of those quantities have the same dimension and can be expressed in the same units, they have different quantity equations that can be used to create them implicitly:

  • energy is the most generic one and thus can be created from base quantities of mass, length, and time. As those are also the roots of quantities of their kinds and all other quantities from their trees are implicitly convertible to them (we agreed on that "every width is a length" already), it means that an energy can be implicitly constructed from any quantity of mass, length, and time:

    static_assert(implicitly_convertible(isq::mass * pow<2>(isq::length) / pow<2>(isq::time), isq::energy));
    static_assert(implicitly_convertible(isq::mass * pow<2>(isq::height) / pow<2>(isq::time), isq::energy));
    
  • mechanical energy is a more "specialized" quantity than energy (not every energy is a mechanical energy). It is why an explicit cast is needed to convert from either energy or the results of its quantity equation:

    static_assert(!implicitly_convertible(isq::energy, isq::mechanical_energy));
    static_assert(explicitly_convertible(isq::energy, isq::mechanical_energy));
    static_assert(!implicitly_convertible(isq::mass * pow<2>(isq::length) / pow<2>(isq::time),
                                          isq::mechanical_energy));
    static_assert(explicitly_convertible(isq::mass * pow<2>(isq::length) / pow<2>(isq::time),
                                         isq::mechanical_energy));
    
  • gravitational potential energy is not only even more specialized one but additionally, it is special in a way that it provides its own "constrained" quantity equation. Maybe not every mass * pow<2>(length) / pow<2>(time) is a gravitational potential energy, but every mass * acceleration_of_free_fall * height is.

    static_assert(!implicitly_convertible(isq::energy, gravitational_potential_energy));
    static_assert(explicitly_convertible(isq::energy, gravitational_potential_energy));
    static_assert(!implicitly_convertible(isq::mass * pow<2>(isq::length) / pow<2>(isq::time),
                                          gravitational_potential_energy));
    static_assert(explicitly_convertible(isq::mass * pow<2>(isq::length) / pow<2>(isq::time),
                                         gravitational_potential_energy));
    static_assert(implicitly_convertible(isq::mass * isq::acceleration_of_free_fall * isq::height,
                                         gravitational_potential_energy));
    

Modeling a quantity kind

In the physical units library, we also need an abstraction describing an entire family of quantities of the same kind. Such quantities have not only the same dimension but also can be expressed in the same units.

To annotate a quantity to represent its kind (and not just a hierarchy tree's root quantity) we introduced a kind_of<> specifier. For example, to express any quantity of length, we need to type kind_of<isq::length>.

Important

isq::length and kind_of<isq::length> are two different things.

Such an entity behaves as any quantity of its kind. This means that it is implicitly convertible to any quantity in a tree.

static_assert(!implicitly_convertible(isq::length, isq::height));
static_assert(implicitly_convertible(kind_of<isq::length>, isq::height));

Additionally, the result of operations on quantity kinds is also a quantity kind:

static_assert(same_type<kind_of<isq::length> / kind_of<isq::time>, kind_of<isq::length / isq::time>>);

However, if at least one equation's operand is not a quantity kind, the result becomes a "strong" quantity where all the kinds are converted to the hierarchy tree's root quantities:

static_assert(!same_type<kind_of<isq::length> / isq::time, kind_of<isq::length / isq::time>>);
static_assert(same_type<kind_of<isq::length> / isq::time, isq::length / isq::time>);

Info

Only a root quantity from the hierarchy tree or the one marked with is_kind specifier in the quantity_spec definition can be put as a template parameter to the kind_of specifier. For example, kind_of<isq::width> will fail to compile. However, we can call get_kind(q) to obtain a kind of any quantity:

static_assert(get_kind(isq::width) == kind_of<isq::length>);