Address remaining comments for magnitudes.rst

This commit is contained in:
Chip Hogg
2022-08-10 23:36:44 +00:00
parent d0325da46a
commit ce3f048456

View File

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