Adapt Aurora's Magnitude docs for mp-units

The major missing work here is to see how the latex math, inside dollar
signs, translates to the docs.

I also added a new constant, `pi`, to make the docs correct: this should
be much more user friendly.

This is not intended to be "complete".  Rather, the goal is to deliver
the raw content so that experts can massage it.
This commit is contained in:
Chip Hogg
2022-08-04 19:13:53 +00:00
parent 642663a676
commit 10b553bdbe
3 changed files with 125 additions and 0 deletions

View File

@@ -17,6 +17,7 @@ Framework Basics
framework/quantity_points
framework/quantity_kinds
framework/dimensions
framework/magnitudes
framework/units
framework/arithmetics
framework/constants

View File

@@ -0,0 +1,119 @@
.. namespace:: units
Magnitudes
==========
The ratio of two Units of the same Dimension---say, `inches` and `centi<meters>`---is some constant
number, which is known at compile time. It's a positive real number---a _Magnitude_.
We also use Magnitudes for _Dimensionless_ Units. `percent` has a Magnitude of $1/100$, and `dozen`
would have a Magnitude of $12$.
Interestingly, it turns out that the usual numeric types are not up to this task. We need
a Magnitude representation that can do everything Units can do. This means, among other things:
1. We need _exact_ symbolic computation of the core operations of Quantity Calculus (i.e., products
and rational powers).
2. We must support _irrational_ Magnitudes, because they frequently occur in practice (e.g.,
consider the ratio between `degrees` and `radians`).
3. We should _avoid overflow_ wherever possible (note that `std::intmax_t` can't even handle certain
simple SI prefixes, such as `yotta`, representing $10^{24}$).
Integers' inadequacies are clear enough, but even floating point falls short. Imagine if we
implemented all angular units in terms of `radians`: then both `degrees` and `revolutions` pick up
a factor of $\pi$. The arithmetic with _its floating point representation_ is unlikely to cancel
_exactly_.
Another common alternative choice is `std::ratio`, but this fails the first requirement: rational
numbers are ([rather infamously](https://hsm.stackexchange.com/a/7)!) _not_ closed under rational
powers.
The only viable solution we have yet encountered is the _vector space representation_. The
implementation is fascinating---but, for purposes of this present page, it's also a distraction.
_Here,_ we're more focused on how to _use_ these Magnitudes.
One type per Magnitude, one value per type
------------------------------------------
Each typical numeric type (`double`, `int64_t`, ...) can represent a wide variety of values: the
more, the better. However, Magnitudes are **not** like that. Instead, they comprise a _variety_ of
types, and each type can hold only _one_ value.
.. tip::
A given Magnitude represents the _same_ number, whether you use it as a _type_, or as a _value_
(i.e., an _instance_ of that type).
Use whichever is more convenient. (In practice, this is usually the _value_: especially for end
users rather than library developers.)
If these types can only represent one value each, why would we bother to instantiate them? Because
_values are easier to use_.
- `mag<N>()` gives the Magnitude value corresponding to any integer `N`.
- You can combine values in the usual way using `*`, `/`, `==`, and `!=`, as well as `pow<N>(m)` and
`root<N>(m)` for any Magnitude value `m`.
Traits: integers and rational Magnitudes
----------------------------------------
If you have a Magnitude instance `m`, we provide traits to help you reason about integers and
rational numbers, or manipulate integer or rational parts.
- `is_integral(m)`: indicates whether `m` represents an _integral_ Magnitude.
- `is_rational(m)`: indicates whether `m` represents a _rational_ Magnitude.
The above traits indicate what kind of Magnitude we already have. The next traits _manipulate_ a
Magnitude, letting us break it apart into _constituent_ Magnitudes which may be more meaningful.
(For example, imagine going from `inches` to `feet`. Naively, we might multiply by the floating
point representation of `1.0 / 12.0`. However, if we broke this apart into separate numerator and
denominator, it would let us simply _divide by 12_, yielding **exact** results for inputs that
happen to be multiples of 12.)
- `numerator(m)` (value): a Magnitude representing the "numerator", i.e., the largest integer which
divides `m`, without turning any of its base powers' exponents negative (or making any
previously-negative exponents _more_ negative).
- `denominator(m)` (value): the "numerator" of the _inverse_ of `m`.
These traits interact as one would hope. For example, `is_rational(m)` is exactly equivalent to
`m == numerator(m) / denominator(m)`.
Why these particular definitions? Because they are equivalent to the numerator and denominator when
we have a rational number, and they are compatible with how humans write numbers when we don't.
Example:
- $m1 = \frac{27 \pi^2}{25}$. Then `numerator(m1) == mag<27>()`, and
`denominator(m1) == mag<25>()`.
- $m2 = \sqrt{m1}$. Then `numerator(m2) == mag<3>()`, and `denominator(m2) == mag<5>()`. Note that
this is consistent with how humans would typically write `m2`, as $\frac{3\sqrt{3} \pi}{5}$.
Getting values out
------------------
Magnitude types represent numbers in non-numeric types. They've got some amazing strengths (exact
rational powers!), and some significant weaknesses (no support for basic addition!). So what if you
just want to turn a Magnitude `m` into a traditional numeric type `T`?
You call `get_value<T>(m)`.
This does what it looks like it does, and it does it at compile time. Any intermediate computations
take place in the "widest type in category"---`long double` for floating point, and `std::intmax_t`
or `std::uintmax_t` for signed or unsigned integers---before ultimately being cast back to the
target type. For `T = float`, say, this means we get all the precision we'd have with something
like `long double`, but without any speed penalty at runtime!
`get_value<T>(m)` also has the protections you would hope: for example, if `T` is an integral type,
we require `is_integral(m)`.
How to use Magnitudes
---------------------
- First, start with your basic inputs: this will typically be `mag<N>()` for any integer `N`, or the
built-in Magnitude constant `pi`. (Again, these are all _values_, not types.)
- Next, combine and manipulate these using the various "Magnitude math" operations, all of which are
**exact**: `*`, `/`, `pow<N>`, `root<N>`, `numerator()`, `denominator()`.
- If you need to translate a Magnitude `m` to a "real" numeric type `T`, call `get_value<T>(m)`.

View File

@@ -398,6 +398,11 @@ struct pi_base {
static constexpr long double value = std::numbers::pi_v<long double>;
};
/**
* @brief A convenient Magnitude constant for pi, which we can manipulate like a regular number.
*/
inline constexpr Magnitude auto pi = magnitude<base_power<pi_base>{}>{};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Magnitude equality implementation.