diff --git a/docs/blog/posts/isq-part-5-benefits.md b/docs/blog/posts/isq-part-5-benefits.md new file mode 100644 index 00000000..ab073c27 --- /dev/null +++ b/docs/blog/posts/isq-part-5-benefits.md @@ -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). + + + +## 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 auto avg_speed(QuantityOf auto d, + QuantityOf 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 {} currency; + +inline constexpr struct euro final : named_unit<"EUR", kind_of> {} euro; +inline constexpr struct us_dollar final : named_unit<"USD", kind_of> {} us_dollar; + +namespace unit_symbols { + +inline constexpr auto EUR = euro; +inline constexpr auto USD = us_dollar; + +} + +static_assert(!std::equality_comparable_with, quantity>); +``` + +Next, we can provide custom currency exchange facility that accounts for a specific point in time: + +```cpp +template +[[nodiscard]] double exchange_rate(std::chrono::sys_seconds timestamp) +{ + // user-provided logic... +} + +template auto To, QuantityOf From> +QuantityOf auto exchange_to(From q, std::chrono::sys_seconds timestamp) +{ + const auto rate = + static_cast(exchange_rate(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(system_clock::now() - hours{24}); +const quantity price_usd = 100 * USD; +const quantity price_euro = exchange_to(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 {} fuel_consumption; +inline constexpr auto L_per_100km = si::litre / (mag<100> * si::kilo); + +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 {} horizontal_length; +inline constexpr struct horizontal_area final : quantity_spec {} 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 length_; + quantity width_; + quantity height_; +public: + Box(quantity l, quantity w, quantity h): + length_(l), width_(w), height_(h) + {} + + quantity 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 {} SampleCount; +inline constexpr struct UnitSampleAmount final : quantity_spec {} UnitSampleAmount; +inline constexpr struct MIDIClock final : quantity_spec {} MIDIClock; +inline constexpr struct BeatCount final : quantity_spec {} 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 {} SampleDuration; +inline constexpr struct SamplingRate final : quantity_spec {} SamplingRate; + +inline constexpr auto Amplitude = UnitSampleAmount; +inline constexpr auto Level = UnitSampleAmount; +inline constexpr struct Power final : quantity_spec {} Power; + +inline constexpr struct BeatDuration final : quantity_spec {} BeatDuration; +inline constexpr struct Tempo final : quantity_spec {} 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> {} Sample; +inline constexpr struct SampleValue final : named_unit<"PCM", one, kind_of> {} SampleValue; +inline constexpr struct MIDIPulse final : named_unit<"p", one, kind_of> {} MIDIPulse; + +inline constexpr struct QuarterNote final : named_unit<"q", one, kind_of> {} 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(Smpl); +const auto rampSamples2 = (rampTime * sr2).force_in(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(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.