mirror of
https://github.com/mpusz/mp-units.git
synced 2025-07-29 18:07:16 +02:00
docs: initial version of ISQ part 5 added
This commit is contained in:
429
docs/blog/posts/isq-part-5-benefits.md
Normal file
429
docs/blog/posts/isq-part-5-benefits.md
Normal file
@ -0,0 +1,429 @@
|
||||
---
|
||||
draft: true
|
||||
date: 2024-11-04
|
||||
authors:
|
||||
- mpusz
|
||||
categories:
|
||||
- Metrology
|
||||
comments: true
|
||||
---
|
||||
|
||||
# International System of Quantities (ISQ): Part 5 - Benefits
|
||||
|
||||
In the previous articles we have introduced the International System of Quantities, described how
|
||||
we can model and implement it in a programming language, and presented the issues of the software
|
||||
that does not use such abstraction to implement a units library.
|
||||
|
||||
In this article we will present how our ISQ model elegantly addresses the issues from the
|
||||
[Part 2](isq-part-2-problems-when-isq-is-not-used.md) of our series that were not covered already
|
||||
in [Part 3](isq-part-3-modelling-isq.md).
|
||||
|
||||
<!-- more -->
|
||||
|
||||
## Articles from this series
|
||||
|
||||
- [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](isq-part-3-modelling-isq.md)
|
||||
- [Part 4 - Implementing ISQ](isq-part-4-implemeting-isq.md)
|
||||
- Part 5 - Benefits
|
||||
|
||||
|
||||
## Generic but safe interfaces
|
||||
|
||||
Let's start with the implementation of a
|
||||
[generic utility function that would calculate the average speed based on provided arguments](isq-part-2-problems-when-isq-is-not-used.md#no-way-to-specify-a-quantity-type-in-generic-interfaces).
|
||||
The resulting quantity should use a derived unit of the provided arguments (e.g., `km/h` for
|
||||
`km` and `h`, `m/s` for `m` and `s`, ...).
|
||||
|
||||
With C++ concepts backed up with ISQ quantities we can simply type it as:
|
||||
|
||||
```cpp
|
||||
constexpr QuantityOf<isq::speed> auto avg_speed(QuantityOf<isq::length> auto d,
|
||||
QuantityOf<isq::time> auto t)
|
||||
{
|
||||
return d / t;
|
||||
}
|
||||
```
|
||||
|
||||
The above constrains the algorithm to proper quantity types and ensures that a quantity of speed
|
||||
is returned. The latter is not only important for the users to better understand what the function
|
||||
does, but also serves as a unit test for our implementation. It ensures that our quantity equations
|
||||
are correct in the implementation part of the function and we indeed return a quantity of _speed_.
|
||||
|
||||
|
||||
## Non-convertible units of currency
|
||||
|
||||
Our second example was about
|
||||
[disjoint units of _currency_](isq-part-2-problems-when-isq-is-not-used.md#disjoint-units-of-the-same-quantity-type-do-not-work).
|
||||
We want to use various units of _currency_ but we can't provide compile-time known conversion
|
||||
factors between those as such ratios are only known at runtime.
|
||||
|
||||
First, we define:
|
||||
|
||||
- a new dimension for _currency_ and quantity type based on it,
|
||||
- set of disjoint units of _currency_ for its quantity kind.
|
||||
|
||||
```cpp
|
||||
inline constexpr struct dim_currency final : base_dimension<"$"> {} dim_currency;
|
||||
inline constexpr struct currency final : quantity_spec<dim_currency> {} currency;
|
||||
|
||||
inline constexpr struct euro final : named_unit<"EUR", kind_of<currency>> {} euro;
|
||||
inline constexpr struct us_dollar final : named_unit<"USD", kind_of<currency>> {} us_dollar;
|
||||
|
||||
namespace unit_symbols {
|
||||
|
||||
inline constexpr auto EUR = euro;
|
||||
inline constexpr auto USD = us_dollar;
|
||||
|
||||
}
|
||||
|
||||
static_assert(!std::equality_comparable_with<quantity<euro, int>, quantity<us_dollar, int>>);
|
||||
```
|
||||
|
||||
Next, we can provide custom currency exchange facility that accounts for a specific point in time:
|
||||
|
||||
```cpp
|
||||
template<Unit auto From, Unit auto To>
|
||||
[[nodiscard]] double exchange_rate(std::chrono::sys_seconds timestamp)
|
||||
{
|
||||
// user-provided logic...
|
||||
}
|
||||
|
||||
template<UnitOf<currency> auto To, QuantityOf<currency> From>
|
||||
QuantityOf<currency> auto exchange_to(From q, std::chrono::sys_seconds timestamp)
|
||||
{
|
||||
const auto rate =
|
||||
static_cast<From::rep>(exchange_rate<From::unit, To>(timestamp) * q.numerical_value_in(q.unit));
|
||||
return rate * From::quantity_spec[To];
|
||||
}
|
||||
```
|
||||
|
||||
Finally, we can use our simple model in the following way:
|
||||
|
||||
```cpp
|
||||
using namespace unit_symbols;
|
||||
using namespace std::chrono;
|
||||
|
||||
const auto yesterday = time_point_cast<seconds>(system_clock::now() - hours{24});
|
||||
const quantity price_usd = 100 * USD;
|
||||
const quantity price_euro = exchange_to<euro>(price_usd, yesterday);
|
||||
|
||||
std::cout << price_usd << " -> " << price_euro << "\n";
|
||||
// std::cout << price_usd + price_euro << "\n"; // does not compile
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
It would be better to model the above prices as quantity points, but this is a subject
|
||||
for a totally different article :wink:.
|
||||
|
||||
|
||||
## Derived quantities of the same dimension but different kinds
|
||||
|
||||
Up until now, the discussed issues did not actually require modelling of the ISQ. Introduction
|
||||
of physical dimensions would be enough, and indeed, this is what most of the libraries on the
|
||||
market do. However, we have more interesting challenges to solve as well.
|
||||
|
||||
The next issue was related to different quantities having the same dimension. In many cases we
|
||||
want to prevent conversions and any other compatibility between such distinct quantities.
|
||||
|
||||
Let's try to implement
|
||||
[our _fuel consumption_ example](isq-part-2-problems-when-isq-is-not-used.md#derived-quantities-of-the-same-dimension-but-different-kinds).
|
||||
First, we define the quantity type and a handy identifier for a derived unit that we want to use:
|
||||
|
||||
```cpp
|
||||
inline constexpr struct fuel_consumption final : quantity_spec<isq::volume / isq::length> {} fuel_consumption;
|
||||
inline constexpr auto L_per_100km = si::litre / (mag<100> * si::kilo<si::metre>);
|
||||
|
||||
static_assert(fuel_consumption != isq::area);
|
||||
static_assert(fuel_consumption.dimension == isq::area.dimension);
|
||||
```
|
||||
|
||||
Next, we define two quantities. The first one is based only on a derived unit of `L/[100 km]`,
|
||||
while the second uses a strongly typed quantity type:
|
||||
|
||||
```cpp
|
||||
quantity q1 = 5.8 * L_per_100km;
|
||||
quantity q2 = fuel_consumption(6.7 * L_per_100km);
|
||||
std::println("Fuel consumptions: {}, {}", q1, q2);
|
||||
|
||||
static_assert(implicitly_convertible(q1.quantity_spec, isq::area));
|
||||
static_assert(!implicitly_convertible(q2.quantity_spec, isq::area));
|
||||
static_assert(!explicitly_convertible(q2.quantity_spec, isq::area));
|
||||
static_assert(!castable(q2.quantity_spec, isq::area));
|
||||
```
|
||||
|
||||
As we can see, with just units (especially derived ones) and dimensions, we often can't achieve
|
||||
the same level of safety as with properly modelled hierarchies of quantities. Only in case of `q2`
|
||||
we can prevent incorrect conversions to a totally different quantity of the same dimension.
|
||||
|
||||
|
||||
## Various quantities of the same dimension and kinds
|
||||
|
||||
In the previous example _area_ and _fuel consumption_ were different quantities of the same
|
||||
dimension but different kinds. In the engineering there are also many cases where we want to model
|
||||
distinct quantities of the same kind.
|
||||
|
||||
Let's try to improve the safety of
|
||||
[our `Box` example](isq-part-2-problems-when-isq-is-not-used.md#various-quantities-of-the-same-dimension-and-kinds).
|
||||
|
||||
First, we need to extend our ISQ definitions by the _horizontal length_ quantity and a
|
||||
_horizontal area_ derived from it:
|
||||
|
||||
```cpp
|
||||
inline constexpr struct horizontal_length final : quantity_spec<isq::length> {} horizontal_length;
|
||||
inline constexpr struct horizontal_area final : quantity_spec<isq::area, horizontal_length * isq::width> {} horizontal_area;
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
`isq::length` denotes any quantity of _length_ (not only the horizontal one).
|
||||
|
||||
```cpp
|
||||
static_assert(implicitly_convertible(horizontal_length, isq::length));
|
||||
static_assert(!implicitly_convertible(isq::length, horizontal_length));
|
||||
|
||||
static_assert(implicitly_convertible(horizontal_area, isq::area));
|
||||
static_assert(!implicitly_convertible(isq::area, horizontal_area));
|
||||
|
||||
static_assert(implicitly_convertible(isq::length * isq::length, isq::area));
|
||||
static_assert(!implicitly_convertible(isq::length * isq::length, horizontal_area));
|
||||
|
||||
static_assert(implicitly_convertible(horizontal_length * isq::width, isq::area));
|
||||
static_assert(implicitly_convertible(horizontal_length * isq::width, horizontal_area));
|
||||
```
|
||||
|
||||
With simple 2 lines of definitions we made all of the above logic automatically work without
|
||||
any need for additional customization for special cases. The proposed model based on
|
||||
hierarchies of derived quantities and their recipes, automatically inherits the properties
|
||||
of base quantities involved in the recipe. This makes the composition of derived quantities
|
||||
very easy which is not the case for alternative solutions based on tag types that do not
|
||||
compose their properties.
|
||||
|
||||
Now we can refactor our `Box` to benefit from the introduced safe abstractions:
|
||||
|
||||
```cpp
|
||||
class Box {
|
||||
quantity<horizontal_length[m]> length_;
|
||||
quantity<isq::width[m]> width_;
|
||||
quantity<isq::height[m]> height_;
|
||||
public:
|
||||
Box(quantity<horizontal_length[m]> l, quantity<isq::width[m]> w, quantity<isq::height[m]> h):
|
||||
length_(l), width_(w), height_(h)
|
||||
{}
|
||||
|
||||
quantity<horizontal_area[m2]> floor() const { return length_ * width_; }
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
It is important to note that the safety can be enforced only when a user provides typed quantities
|
||||
as arguments to the functions:
|
||||
|
||||
```cpp
|
||||
Box my_box1(2 * m, 3 * m, 1 * m);
|
||||
Box my_box2(2 * horizontal_length[m], 3 * isq::width[m], 1 * isq::height[m]);
|
||||
Box my_box3(horizontal_length(2 * m), isq::width(3 * m), isq::height(1 * m));
|
||||
```
|
||||
|
||||
!!! important
|
||||
|
||||
It is up to the user to decide when and where to care about explicit quantity types
|
||||
and when to prefer simple unit-only mode.
|
||||
|
||||
|
||||
## Various kinds of dimensionless quantities
|
||||
|
||||
Most of the quantities hierarchies describe only one kind. There are some exceptions, though.
|
||||
One of them is a [hierarchy of _dimensionless_ quantities](#modeling-a-hierarchy-of-kind-dimensionless).
|
||||
This tree defines quantities that denote:
|
||||
|
||||
- counts (_storage capacity_),
|
||||
- ratios (_efficiency_),
|
||||
- angles (_angular measure_, _solid angular measure_),
|
||||
- scaled numbers.
|
||||
|
||||
Each of the above could form a separate tree of mutually comparable quantities. However, all of
|
||||
them have a common property. Every quantity from this tree, despite often being measured in a
|
||||
dedicated unit (e.g., `bit`, `rad`, `sr`), should also be able to be measured in a unit `one`.
|
||||
|
||||
We've seen how to model such a hierarchy in a previous article in our series. This time, we will
|
||||
see a simplified part of a concrete real-life example for this use cases.
|
||||
|
||||
In the digital signal processing domain we need to provide strong types for different counts.
|
||||
Abstractions like _samples_, _beats_, _MIDI clock_, and others should not be possible to be
|
||||
intermixed with each other:
|
||||
|
||||
```cpp
|
||||
namespace ni {
|
||||
|
||||
inline constexpr struct SampleCount final : quantity_spec<dimensionless, is_kind> {} SampleCount;
|
||||
inline constexpr struct UnitSampleAmount final : quantity_spec<dimensionless, is_kind> {} UnitSampleAmount;
|
||||
inline constexpr struct MIDIClock final : quantity_spec<dimensionless, is_kind> {} MIDIClock;
|
||||
inline constexpr struct BeatCount final : quantity_spec<dimensionless, is_kind> {} BeatCount;
|
||||
```
|
||||
|
||||
We should also be able to create derived quantities from those. For example, when we divide such
|
||||
a quantity by time we should get a new strong quantity that can be measured in both a dedicated
|
||||
unit (e.g., `Smpl/s` for _sample rate_) and hertz:
|
||||
|
||||
```cpp
|
||||
inline constexpr struct SampleDuration final : quantity_spec<isq::period_duration> {} SampleDuration;
|
||||
inline constexpr struct SamplingRate final : quantity_spec<isq::frequency, SampleCount / SampleDuration> {} SamplingRate;
|
||||
|
||||
inline constexpr auto Amplitude = UnitSampleAmount;
|
||||
inline constexpr auto Level = UnitSampleAmount;
|
||||
inline constexpr struct Power final : quantity_spec<Level * Level> {} Power;
|
||||
|
||||
inline constexpr struct BeatDuration final : quantity_spec<isq::period_duration> {} BeatDuration;
|
||||
inline constexpr struct Tempo final : quantity_spec<isq::frequency, BeatCount / BeatDuration> {} Tempo;
|
||||
```
|
||||
|
||||
We can also define a collection of units associated with specific quantity kinds and their symbols:
|
||||
|
||||
```cpp
|
||||
inline constexpr struct Sample final : named_unit<"Smpl", one, kind_of<SampleCount>> {} Sample;
|
||||
inline constexpr struct SampleValue final : named_unit<"PCM", one, kind_of<UnitSampleAmount>> {} SampleValue;
|
||||
inline constexpr struct MIDIPulse final : named_unit<"p", one, kind_of<MIDIClock>> {} MIDIPulse;
|
||||
|
||||
inline constexpr struct QuarterNote final : named_unit<"q", one, kind_of<BeatCount>> {} QuarterNote;
|
||||
inline constexpr struct HalfNote final : named_unit<"h", mag<2> * QuarterNote> {} HalfNote;
|
||||
inline constexpr struct DottedHalfNote final : named_unit<"h.", mag<3> * QuarterNote> {} DottedHalfNote;
|
||||
inline constexpr struct WholeNote final : named_unit<"w", mag<4> * QuarterNote> {} WholeNote;
|
||||
inline constexpr struct EightNote final : named_unit<"8th", mag_ratio<1, 2> * QuarterNote> {} EightNote;
|
||||
inline constexpr struct DottedQuarterNote final : named_unit<"q.", mag<3> * EightNote> {} DottedQuarterNote;
|
||||
inline constexpr struct QuarterNoteTriplet final : named_unit<"qt", mag_ratio<1, 3> * HalfNote> {} QuarterNoteTriplet;
|
||||
inline constexpr struct SixteenthNote final : named_unit<"16th", mag_ratio<1, 2> * EightNote> {} SixteenthNote;
|
||||
inline constexpr struct DottedEightNote final : named_unit<"q.", mag<3> * SixteenthNote> {} DottedEightNote;
|
||||
|
||||
inline constexpr auto Beat = QuarterNote;
|
||||
|
||||
inline constexpr struct BeatsPerMinute final : named_unit<"bpm", Beat / si::minute> {} BeatsPerMinute;
|
||||
inline constexpr struct MIDIPulsePerQuarter final : named_unit<"ppqn", MIDIPulse / QuarterNote> {} MIDIPulsePerQuarter;
|
||||
|
||||
namespace unit_symbols {
|
||||
|
||||
inline constexpr auto Smpl = Sample;
|
||||
inline constexpr auto pcm = SampleValue;
|
||||
inline constexpr auto p = MIDIPulse;
|
||||
|
||||
inline constexpr auto n_wd = 3 * HalfNote;
|
||||
inline constexpr auto n_w = WholeNote;
|
||||
inline constexpr auto n_hd = DottedHalfNote;
|
||||
inline constexpr auto n_h = HalfNote;
|
||||
inline constexpr auto n_qd = DottedQuarterNote;
|
||||
inline constexpr auto n_q = QuarterNote;
|
||||
inline constexpr auto n_qt = QuarterNoteTriplet;
|
||||
inline constexpr auto n_8thd = DottedEightNote;
|
||||
inline constexpr auto n_8th = EightNote;
|
||||
inline constexpr auto n_16th = SixteenthNote;
|
||||
|
||||
}
|
||||
|
||||
} // namespace ni
|
||||
```
|
||||
|
||||
With the above we can work with each quantity in a safe way and use SI or domain-specific units
|
||||
as needed:
|
||||
|
||||
```cpp
|
||||
using namespace ni::unit_symbols;
|
||||
using namespace mp_units::si::unit_symbols;
|
||||
|
||||
const auto sr1 = ni::GetSampleRate();
|
||||
const auto sr2 = 48'000.f * Smpl / s;
|
||||
|
||||
const auto samples = 512 * Smpl;
|
||||
|
||||
const auto sampleTime1 = (samples / sr1).in(s);
|
||||
const auto sampleTime2 = (samples / sr2).in(ms);
|
||||
|
||||
const auto sampleDuration1 = (1 / sr1).in(ms);
|
||||
const auto sampleDuration2 = (1 / sr2).in(ms);
|
||||
|
||||
const auto rampTime = 35.f * ms;
|
||||
const auto rampSamples1 = (rampTime * sr1).force_in<int>(Smpl);
|
||||
const auto rampSamples2 = (rampTime * sr2).force_in<int>(Smpl);
|
||||
|
||||
std::println("Sample rate 1 is: {}", sr1);
|
||||
std::println("Sample rate 2 is: {}", sr2);
|
||||
|
||||
std::println("{} @ {} is {::N[.5f]}", samples, sr1, sampleTime1);
|
||||
std::println("{} @ {} is {::N[.5f]}", samples, sr2, sampleTime2);
|
||||
|
||||
std::println("One sample @ {} is {::N[.5f]}", sr1, sampleDuration1);
|
||||
std::println("One sample @ {} is {::N[.5f]}", sr2, sampleDuration2);
|
||||
|
||||
std::println("{} is {} @ {}", rampTime, rampSamples1, sr1);
|
||||
std::println("{} is {} @ {}", rampTime, rampSamples2, sr2);
|
||||
```
|
||||
|
||||
The above prints:
|
||||
|
||||
```text
|
||||
Sample rate 1 is: 44100 Hz
|
||||
Sample rate 2 is: 48000 Smpl/s
|
||||
512 Smpl @ 44100 Hz is 0.01161 s
|
||||
512 Smpl @ 48000 Smpl/s is 10.66667 ms
|
||||
One sample @ 44100 Hz is 0.02268 ms
|
||||
One sample @ 48000 Smpl/s is 0.02083 ms
|
||||
35 ms is 1543 Smpl @ 44100 Hz
|
||||
35 ms is 1680 Smpl @ 48000 Smpl/s
|
||||
```
|
||||
|
||||
We can also do a bit more advanced computations to get the following:
|
||||
|
||||
```cpp
|
||||
auto sampleValue = -0.4f * pcm;
|
||||
auto power1 = sampleValue * sampleValue;
|
||||
auto power2 = -0.2 * pow<2>(pcm);
|
||||
|
||||
auto tempo = ni::GetTempo();
|
||||
auto reverbBeats = 1 * n_qd;
|
||||
auto reverbTime = reverbBeats / tempo;
|
||||
|
||||
auto pulsePerQuarter = value_cast<float>(ni::GetPPQN());
|
||||
auto transportPosition = ni::GetTransportPos();
|
||||
auto transportBeats = (transportPosition / pulsePerQuarter).in(n_q);
|
||||
auto transportTime = (transportBeats / tempo).in(s);
|
||||
|
||||
std::println("SampleValue is: {}", sampleValue);
|
||||
std::println("Power 1 is: {}", power1);
|
||||
std::println("Power 2 is: {}", power2);
|
||||
|
||||
std::println("Tempo is: {}", tempo);
|
||||
std::println("Reverb Beats is: {}", reverbBeats);
|
||||
std::println("Reverb Time is: {}", reverbTime.in(s));
|
||||
std::println("Pulse Per Quarter is: {}", pulsePerQuarter);
|
||||
std::println("Transport Position is: {}", transportPosition);
|
||||
std::println("Transport Beats is: {}", transportBeats);
|
||||
std::println("Transport Time is: {}", transportTime);
|
||||
```
|
||||
|
||||
which prints:
|
||||
|
||||
```text
|
||||
SampleValue is: -0.4 PCM
|
||||
Power 1 is: 0.16000001 PCM²
|
||||
Power 2 is: -0.2 PCM²
|
||||
Tempo is: 110 bpm
|
||||
Reverb Beats is: 1 q.
|
||||
Reverb Time is: 0.8181818 s
|
||||
Pulse Per Quarter is: 960 ppqn
|
||||
Transport Position is: 15836 p
|
||||
Transport Beats is: 16.495832 q
|
||||
Transport Time is: 8.997726 s
|
||||
```
|
||||
|
||||
!!! info
|
||||
|
||||
More about this example can be found in
|
||||
["Exploration of Strongly-typed Units in C++: A Case Study from Digital Audio"](https://www.youtube.com/watch?v=oxnCdIfC4Z4)
|
||||
CppCon 2023 talk by Roth Michaels.
|
||||
|
||||
|
||||
## 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.
|
Reference in New Issue
Block a user