docs: ISQ parts 1-3 improved

This commit is contained in:
Mateusz Pusz
2024-10-01 12:07:19 +02:00
parent e7663fe9fd
commit 1ff527b5e8
3 changed files with 114 additions and 75 deletions

View File

@ -73,8 +73,8 @@ with other such systems. For example:
Both **systems of units** above agree on the unit of _time_, but chose different units for other
quantities. In the above example, SI chose a non-prefixed unit of metre for a base quantity of _length_
while CGS chose a scaled centimetre. On the other hand, SI chose a scaled kilogram over the gram used
in the CGS. Those decisions also result in a need for different units for derived quantities.
For example:
in the CGS. Those decisions also result in a need for different [coherent units](https://jcgm.bipm.org/vim/en/1.12.html)
for derived quantities. For example:
| Quantity | SI | CGS |
|------------|---------------|-----------------|
@ -87,7 +87,7 @@ For example:
Often, there is no way to state which one is correct or which one is wrong. Each
**system of units** has the freedom to choose whichever unit suits its engineering requirements
and constraints the best.
and constraints the best for a specific quantity.
## ISQ vs SI

View File

@ -19,25 +19,25 @@ systems, in this article, we will talk about the benefits we get from modeling i
The issues described in this article do not apply to the **mp-units** library. Its interfaces,
even if when we decide only to use [simple quantities](../../users_guide/framework_basics/simple_and_typed_quantities.md)
that only use units, those are still backed up by quantity kinds under the framework's hood._
that only use units, those are still backed up by quantity kinds under the framework's hood.
## Articles from this series
Previous:
- [Part 1 - Introduction](isq-part-1-introduction.md)
- Part 2 - Problems when ISQ is not used
## Limitations of units-only solutions
Units-only is not a good design for a quantities and units library. It works to some extent, but
plenty of use cases can't be addressed, and for those that somehow work, we miss important safety improvements provided by additional abstractions in this article.
plenty of use cases can't be addressed, and for those that somehow work, we miss important safety
improvements provided by additional abstractions in this article series.
### No way to specify a quantity type in generic interfaces
A common requirement in the domain is to write unit-agnostic generic interfaces. For example,
let's try to implement a generic `avg_speed` function template that takes a quantity of any
unit and produces the result. So if we call it with _distance_ in `km` and _time_ in `h`, we will
unit and produces the result. If we call it with _distance_ in `km` and _time_ in `h`, we will
get `km/h` as a result, but if we call it with `mi` and `h`, we expect `mi/h` to be returned.
```cpp
@ -71,29 +71,36 @@ avg_speed(120 * km, 2 * h).in(km / h);
Despite being safer, the above code decreased the performance because we always pay for the
conversion at the function's input and output.
We could try to provide concepts like `ScaledUnitOf<si::metre>` that will try to constrain
the arguments somehow, but it leads to even more problems with the unit definitions. For example,
are `Hz` and `Bq` just scaled versions of `1/s`? What about radian and steradian or a litre and
a cubic meter?
Moreover, in a good library, the above code should not compile. The reason for this is that
even though the conversion from `km` to `m` and from `h` to `s` is considered value-preserving,
it is not true in the opposite direction. When we try to convert the result stored in an
integral type from the unit of `m/s` to `km/h`, we will inevitably lose some data.
We could try to provide concepts like `ScaledUnitOf<si::metre>` that would take a set of units
while trying to constrain them somehow, but it leads to even more problems with the unit
definitions. For example, are `Hz` and `Bq` just scaled versions of `1/s`? If we constrain the
interface to just prefixed units, then litre and a cubic metre or kilometre and mile will be
incompatible. What about radian and steradian or a litre per 100 kilometre (popular unit of
a fuel consumption) and a squared metre? Should those be compatible?
### Disjoint units of the same quantity type do not work
Sometimes, we need to define several units describing the same quantity but which do not convert
to each other. A typical example can be a currency use case. A user may want to define EURO and
USD as units of currency, but do not provide any predefined conversion factor and handle such
a conversion at runtime with custom logic (e.g., using an additional time point function argument).
In such a case, how can we specify that EURO and USD are quantities of the same type/dimension?
Sometimes, we need to define several units describing the same quantity but which should not
convert to each other in the library's framework. A typical example here is currency. A user
may want to define EURO and USD as units of currency, so both of them can be used for such
quantities. However, it is impossible to predefine one fixed conversion factor for those,
as a currency exchange rate varies over time, and the library's framework can't provide such
an information as an input to the built-in conversion function. User's application may have more
information in this domain and handle such a conversion at runtime with custom logic
(e.g., using an additional time point function argument). If we would like to model that
in a unit-only solution, how can we specify that EURO and USD are units of quantities of
currency, but are not convertible to each other?
## Dimensions to the rescue?
To prevent the above issues, most of the libraries on the market introduce dimension abstraction.
To resolve the above issues, most of the libraries on the market introduce dimension abstraction.
Thanks to that, we could solve the first issue of the previous chapter with:
```cpp
@ -149,7 +156,7 @@ For example:
steradian (sr) is a unit of _solid angle_ defined as $m^2/m^2$.
Both are quantities of dimension one, which also has its own units like one (1) and percent (%).
There are many more similar examples in the ISO 80000 series. For example, _storage capacity_
There are many more similar examples in the ISO/IEC 80000 series. For example, _storage capacity_
quantity can be measured in units of one, bit, octet, and byte.
The above conflicts can't be solved with dimensions, and they yield many safety issues. For example,
@ -177,12 +184,32 @@ Again, we don't want to accidentally mix those.
### Various quantities of the same dimension and kinds
Even if we somehow address all the above, there are still plenty of use cases that still can't be
safely implemented with such abstractions.
Even if we somehow address all the above, there are plenty of use cases that still can't be safely
implemented with such abstractions.
Let's consider that we want to implement a freight transport application to position cargo in the
container. In such a scenario, we need to be able to discriminate between _length_, _width_, and
_height_ of the package. Also, often, we can find a "This side up" arrow on the box.
container. In majority of the products on the market we will end up with something like:
```cpp
class Box {
length length_;
length width_;
length height_;
public:
Box(length l, length w, length h): length_(l), width_(w), height_(h) {}
area floor() const { return length_ * width_; }
// ...
};
```
```cpp
Box my_box(2 * m, 3 * m, 1 * m);
```
Such interfaces are not much safer than just using plain fundamental types (e.g., `double`). One
of the main reasons of using a quantities and units library was to introduce strong-type interfaces
to prevent such issues. In this scenario, we need to be able to discriminate between _length_,
_width_, and _height_ of the package.
A similar but also really important use case is in aviation. The current _altitude_ is a totally
different quantity than the _distance_ to the destination. The same is true for _forward speed_
@ -197,6 +224,22 @@ to make it clear that something potentially unsafe is being done in the code. Al
be able to assign a _potential energy_ to a quantity of _kinetic energy_. However, both of them
(possibly accumulated with each other) should be convertible to a _mechanical energy_ quantity.
```cpp
mass m = 1 * kg;
length l = 1 * m;
time t = 1 * s;
acceleration_of_free_fall g = 9.81 * m / s2;
height h = 1 * m;
speed v = 1 * m / s;
energy e = m * pow<2>(l) / pow<2>(t); // OK
potential_energy ep1 = e; // should not compile
potential_energy ep2 = static_cast<potential_energy>(e); // OK
potential_energy ep3 = m * g * h; // OK
kinetic_energy ek1 = m * pow<2>(v) / 2; // OK
kinetic_energy ek2 = ep3 + ek1; // should not compile
mechanical_energy me = ep3 + ek1; // OK
```
Yet another example comes from the audio industry. In the audio software, we want to treat specific
counts (e.g., _beats_, _samples_) as separate quantities. We could assign dedicated base dimensions
to them. However, if we divide them by _duration_, we should obtain a quantity convertible to
@ -205,10 +248,10 @@ approach, this wouldn't work as the dimension of frequency is just $T^{-1}$, whi
the results of our dimensional equations. This is why we can't assign dedicated dimensions to such
counts.
The last example that we want to mention here comes from finance. This time, we need to model _volume_
as a special quantity of _currency_. _volume_ can be obtained by multiplying _currency_ by the
dimensionless _market quantity_. Of course, both _currency_ and _volume_ should be expressed in
the same units (e.g., USD).
The last example that we want to mention here comes from finance. This time, we need to model
_currency volume_ as a special quantity of _currency_. _currency volume_ can be obtained by
multiplying _currency_ by the dimensionless _market quantity_. Of course, both _currency_ and
_currency volume_ should be expressed in the same units (e.g., USD).
None of the above scenarios can be addressed with just units and dimensions. We need a better
abstraction to safely implement them.
@ -216,4 +259,4 @@ abstraction to safely implement them.
## To be continued...
In the next part of this series, we will introduce the main ideas behind the International
System of Quantities and provide solutions to the problems described above.
System of Quantities and describe how we can model it in the programming language.

View File

@ -23,16 +23,16 @@ language.
## Articles from this series
Previous:
- [Part 1 - Introduction](isq-part-1-introduction.md)
- [Part 2 - Problems when ISQ is not used](isq-part-2-problems-when-isq-is-not-used.md)
- Part 3 - Modelling ISQ
## Dimension is not enough to describe a quantity
Most of the products on the market are aware of physical dimensions. However, a dimension is not
enough to describe a quantity. For example, let's see the following implementation:
enough to describe a quantity. Let's repeat briefly some of the problems described in more detail
in the previous article. For example, let's see the following implementation:
```cpp
class Box {
@ -47,10 +47,10 @@ Box my_box(2 * m, 3 * m, 1 * m);
```
How do you like such an interface? It turns out that in most existing strongly-typed libraries
this is often the best we can do :woozy_face:
this is often the best we can do. :woozy_face:
Another typical question many users ask is how to deal with _work_ and _torque_.
Both of those have the same dimension but are different quantities.
Both of those have the same dimension but are distinct quantities.
A similar issue is related to figuring out what should be the result of:
@ -68,19 +68,15 @@ All of those quantities have the same dimension, namely $\mathsf{T}^{-1}$, but p
is not wise to allow adding, subtracting, or comparing them, as they describe vastly different
physical properties.
If the above example seems too abstract, let's consider _fuel consumption_ (fuel _volume_
divided by _distance_, e.g., `6.7 l/km`) and an _area_. Again, both have the same dimension
$\mathsf{L}^{2}$, but probably it wouldn't be wise to allow adding, subtracting, or comparing
a _fuel consumption_ of a car and the _area_ of a football field. Such an operation does not
have any physical sense and should fail to compile.
If the above example seems too abstract, let's consider Gy (gray - unit of _absorbed dose_)
and Sv (sievert - unit of _dose equivalent_), or radian and steradian. All of them have the
same dimensions.
!!! 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_, ...)
Another example here is _fuel consumption_ (fuel _volume_ divided by _distance_, e.g.,
`6.7 l/100km`) and an _area_. Again, both have the same dimension $\mathsf{L}^{2}$, but probably
it wouldn't be wise to allow adding, subtracting, or comparing a _fuel consumption_ of a car
and the _area_ of a football field. Such an operation does not have any physical sense and should
fail to compile.
It turns out that the above issues can't be solved correctly without proper modeling of
a [system of quantities](../../appendix/glossary.md#system-of-quantities).
@ -89,9 +85,8 @@ a [system of quantities](../../appendix/glossary.md#system-of-quantities).
## Quantities of the same kind
As it was described in the previous article, dimension is not enough to describe a quantity.
We need a better abstraction to ensure the safety of our calculations.
The ISO 80000-1:2009 says:
We need a better abstraction to ensure the safety of our calculations. It turns out that
ISO/IEC 80000 comes with the answer:
!!! quote "ISO 80000-1:2009"
@ -125,9 +120,9 @@ article.
More than one quantity may be defined for the same dimension:
- quantities of different kinds (e.g., _frequency_, _modulation rate_, _activity_)
- 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_)
_wavelength_, _position vector_).
Two quantities can't be added, subtracted, or compared unless they belong to
the same [kind](../../appendix/glossary.md#kind). As _frequency_, _activity_, and _modulation rate_
@ -140,7 +135,7 @@ ISO/IEC 80000 specifies hundreds of different quantities. Plenty of various kind
and often, each kind contains more than one quantity. It turns out that such quantities form
a hierarchy of quantities of the same kind.
For example, here are all quantities of the kind length provided in the ISO 80000-1:
For example, here are all quantities of the kind length provided in the ISO 80000-3:
```mermaid
flowchart TD
@ -186,12 +181,12 @@ Based on the hierarchy above, we can define the following quantity conversion ru
Implicit conversions are allowed on copy-initialization:
```cpp
void foo(quantity<isq::length<m>> q);
void foo(quantity<isq::length[m]> q);
```
```cpp
quantity<isq::width<m>> q1 = 42 * m;
quantity<isq::length<m>> q2 = q1; // implicit quantity conversion
quantity<isq::width[m]> q1 = 42 * m;
quantity<isq::length[m]> q2 = q1; // implicit quantity conversion
foo(q1); // implicit quantity conversion
```
@ -213,12 +208,12 @@ Based on the hierarchy above, we can define the following quantity conversion ru
type:
```cpp
void foo(quantity<isq::height<m>> q);
void foo(quantity<isq::height[m]> q);
```
```cpp
quantity<isq::length<m>> q1 = 42 * m;
quantity<isq::height<m>> q2 = isq::height(q1); // explicit quantity conversion
quantity<isq::length[m]> q1 = 42 * m;
quantity<isq::height[m]> q2 = isq::height(q1); // explicit quantity conversion
foo(isq::height(q1)); // explicit quantity conversion
```
@ -236,12 +231,12 @@ Based on the hierarchy above, we can define the following quantity conversion ru
Explicit casts are forced with a dedicated `quantity_cast` function:
```cpp
void foo(quantity<isq::height<m>> q);
void foo(quantity<isq::height[m]> q);
```
```cpp
quantity<isq::width<m>> q1 = 42 * m;
quantity<isq::height<m>> q2 = quantity_cast<isq::height>(q1); // explicit quantity cast
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
```
@ -262,7 +257,7 @@ Based on the hierarchy above, we can define the following quantity conversion ru
```
```cpp
quantity<isq::length<m>> q1 = 42 * s; // Compile-time error
quantity<isq::length[m]> q1 = 42 * s; // Compile-time error
foo(quantity_cast<isq::length>(42 * s)); // Compile-time error
```
@ -272,7 +267,7 @@ Based on the hierarchy above, we can define the following quantity conversion ru
ISO/IEC 80000 explicitly states that _width_ and _height_ are quantities of the same kind,
and as such they:
- are mutually comparable, and
- are mutually comparable,
- can be added and subtracted.
This means that we should be allowed to compare any quantities from the same tree (as long as
@ -301,7 +296,7 @@ quantities of the same kind. Such quantities have not only the same dimension bu
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
we introduced a `kind_of<>` specifier. For example, to express any quantity of _length_, we need
to type `kind_of<isq::length>`.
!!! important
@ -344,11 +339,12 @@ static_assert(same_type<kind_of<isq::length> / isq::time, isq::length / isq::tim
## How do systems of units benefit from the ISQ and quantity kinds?
Modeling a system of units is the most essential feature and a selling point of every
physical units library. Thanks to that, the library can protect users from performing invalid
operations on quantities and provide automated conversion factors between various compatible units.
Modeling a system of units is the most essential feature and a selling point of every physical
units library. Thanks to that, the library can protect users from assigning, adding, subtracting,
or comparing incompatible units and provide automated conversion factors between various compatible
units.
Probably all the libraries in the wild model the SI, or at least most of it, and many of them
Probably all the libraries in the wild model the SI (or at least most of it), and many of them
provide support for additional units belonging to various other systems (e.g., imperial).
### Systems of units are based on systems of quantities
@ -377,9 +373,9 @@ the amount of any quantity of kind _length_.
where both _length_ and _time_ will be measured in seconds, and _speed_ will be a quantity
measured with the unit `one`. In such case, the definition will look as follows:
```cpp
inline constexpr struct second final : named_unit<"s"> {} second;
```
```cpp
inline constexpr struct second final : named_unit<"s"> {} second;
```
### Constraining a derived unit to work only with a specific derived quantity
@ -406,7 +402,7 @@ for quantities of _activity_:
```cpp
quantity<isq::frequency[Hz]> q1 = 60 * Bq; // Compile-time error
quantity<isq::activity[Hz]> q2; // Compile-time error
quantity<isq::frequency[Hz]> q3 = 60 * Hz;
quantity<isq::frequency[Hz]> q3 = 60 * Hz; // OK
std::cout << q3.in(Bq) << "\n"; // Compile-time error
```
@ -418,10 +414,10 @@ specific kinds only:
auto q = 1 * Hz + 1 * Bq; // Fails to compile
```
All of the above features improve the safety of our library and the products that use it.
All of the above features improve the safety of our library and the products that are using it.
## To be continued...
In the next part of this series, we will discuss the challenges and issues related to the modelling
of the ISQ with a programming language.
In the next part of this series, we will present how our ISQ model helps to address the remaining
issues described in the [Part 2](isq-part-2-problems-when-isq-is-not-used.md) of our series.