Magnitude¶
Magnitude is a family of monovalue types representing nonzero real
numbers. These values can be multiplied, divided, and raised to (rational) powers, and this
arithmetic always takes place at compile time. Values can also be converted to more standard
numeric types, such as double and int, as long as the receiving type can represent the
magnitude’s value faithfully.
The core motivation is to represent ratios of different units that have the same dimension. As
a corollary, any unit can be scaled by a Magnitude to make a new unit of the same dimension.
Forming magnitudes¶
There are 3 valid ways for end users to form a Magnitude instance.
Using the
mag<N>()helper to form the canonical representation of the integerN.Writing
Magnitude<MyConstant>{}, whereMyConstantis a valid irrational magnitude base. (See the custom bases section below for more details.)Forming products, quotients, powers, and roots of other valid
Magnitudeinstances.
The following is a valid, but dis-preferred way to form a Magnitude.
Magnitude<>.- Explanation: This represents the number 1, but it’s less readable than writing
mag<1>().
- Explanation: This represents the number 1, but it’s less readable than writing
The following are not valid ways to form a Magnitude.
Magnitude<Pi, MyConstant>.- Explanation: Do not supply a manual sequence of template parameters.
Magnitudehas strict ordering requirements on its template parameters. The approved methods listed above are guaranteed to satisfy these requirements.
- Explanation: Do not supply a manual sequence of template parameters.
Magnitude<Prime<3>>.- Explanation: Do not supply integer bases manually. Integers are represented by their
prime factorization, which is performed automatically. Instead, form integers, rationals, and
their powers only by starting with valid
Magnitudeinstances, and performing arithmetic operations as in option 3 above.
- Explanation: Do not supply integer bases manually. Integers are represented by their
prime factorization, which is performed automatically. Instead, form integers, rationals, and
their powers only by starting with valid
Below, we give more details on several concepts mentioned above.
mag<N>()¶
mag<N>() gives an instance of the unique, canonical Magnitude type that represents the positive
integer N.
More detail on integral Magnitude representations
Integers are stored as their prime factorization. For example, 18 would be stored as the type
Magnitude<Prime<2>, Pow<Prime<3>, 2>>, because 18 = 2 \cdot 3^2.
mag<N>() automatically performs the prime factorization of N, and constructs a well-formed
Magnitude.
Custom bases¶
Magnitude can handle some irrational numbers. This even includes some transcendental numbers,
such as \pi. Because Magnitude is closed under products and rational powers, this means that we
also automatically support related values such as \pi^2, \frac{1}{\sqrt{2\pi}}, and so on.
What irrational numbers can Magnitude not handle?
A common example is any that are formed by addition. For example, (1 + \sqrt{2}) cannot be
represented by Magnitude. Recall that Magnitude is designed to support products and
rational powers, since these are the most important operations in quantity calculus.
It is tempting to want a better representation — one which supports full symbolic algebra.
Perhaps such a representation could be designed. However, we haven’t seen any real world use
cases for it. The current Magnitude implementation already handles the most critical use
cases, such as handling \pi, which most units libraries have traditionally struggled to
support.
Because of its importance for angular variables, \pi is supported natively in the library, via the
irrational magnitude base, Pi. To define a magnitude instance for \pi, you can write:
If you need to represent an irrational number which can’t be formed via any product of powers of the
existing Magnitude types — namely, integers and \pi — then you can define a new irrational
magnitude base. This is a struct with the following member:
static constexpr long double value(): the best approximation of your constant’s value in thelong doublestorage type.
Important information for defining your own constant
If you return a literal, you must add L on the end. Otherwise it will be interpreted as
double, and will lose precision.
Here are the results of one example which was run on an arbitrary development machine.
| No suffix | L suffix |
|
|---|---|---|
| Literal | 3.141592653589793238 |
3.141592653589793238L |
| Actual Value | 3.141592653589793115 |
3.141592653589793238 |
The un-suffixed version has lost several digits of precision. (The precise amount will depend on the computer architecture being used.)
Each time you add a new irrational magnitude base, you must make sure that it’s independent:
that is, that it can’t be formed as any product of rational powers of existing Magnitude types.
Extracting values¶
As a monovalue type, Magnitude can only hold one value. There are
no computations we can perform at runtime; everything happens at compile time. What we can do is
to extract that represented value, and store it in a more conventional numeric type, such as int
or double.
To extract the value of a Magnitude instance m into a given numeric type T, call
get_value<T>(m). Here are some important aspects of this utility.
- The computation takes place completely at compile time.
- The computation takes place in the widest type of the same kind. (That is, when
Tis floating point we uselong double, and whenTis integral we usestd::intmax_torstd::uintmax_taccording to the signedness ofT.) - If
Tcannot hold the value represented bym, we produce a compile time error.
Example: float and \pi^3
Suppose you are running on an architecture which has hardware support for float, but uses slow
software emulation for double and long double. With Magnitude and get_value, you can
get the best of both worlds:
- The computation gets performed at compile time in
long double, giving extra precision. - The result gets cast to
floatand stored as a program constant.
Thus, if you have a magnitude instance PI, then get_value<float>(pow<3>(PI)) will be much
more accurate than storing \pi in a float, and cubing it — yet, there will be no loss in
runtime performance.
Checking for representability¶
If you need to check whether your magnitude m can be represented in a type T, you can call
representable_in<T>(m). This function is constexpr compatible.
Example: integer and non-integer values
Here are some example test cases which will pass.
Example: range of the type
Here are some example test cases which will pass.
Note that this function’s return value also depends on whether we can compute the value, not just
whether it is representable. For example, representable_in<double>(sqrt(mag<2>())) is currently
false, because we haven’t yet added support for computing rational base powers.
Operations¶
These are the operations which Magnitude supports. Because it is a monovalue
type, the value can take the form of either a type or an instance.
In what follows, we’ll use this convention:
- Capital identifiers (
M,M1,M2, …) refer to types. - Lowercase identifiers (
m,m1,m2, …) refer to instances.
Equality comparison¶
Result: A bool indicating whether two Magnitude values represent the same number.
Syntax:
- For types
M1andM2:std::is_same<M1, M2>::value
- For instances
m1andm2:m1 == m2(equality comparison)m1 != m2(inequality comparison)
Multiplication¶
Result: The product of two Magnitude values.
Syntax:
- For types
M1andM2:MagProductT<M1, M2>
- For instances
m1andm2:m1 * m2
Division¶
Result: The quotient of two Magnitude values.
Syntax:
- For types
M1andM2:MagQuotientT<M1, M2>
- For instances
m1andm2:m1 / m2
Negation¶
Result: The negative of a Magnitude.
Syntax:
- For a type
M:- No special support, but you can form the product with
Magnitude<Negative>, which represents-1.
- No special support, but you can form the product with
- For an instance
m:-m
Powers¶
Result: A Magnitude raised to an integral power.
Syntax:
- For a type
M, and an integral powerN:MagPowerT<M, N>
- For an instance
m, and an integral powerN:pow<N>(m)
Roots¶
Result: An integral root of a Magnitude.
Syntax:
- For a type
M, and an integral rootN:MagPowerT<M, 1, N>(because the N^\text{th} root is equivalent to the \left(\frac{1}{N}\right)^\text{th} power)
- For an instance
m, and an integral rootN:root<N>(m)
Note
If m is negative, and N is even, then root<N>(m) produces a hard compiler error, because
the result cannot be represented as a Magnitude.
Helpers for powers and roots¶
Magnitudes support all of the power helpers. So, for example, for
a magnitude instance m, you can write sqrt(m) as a more readable alternative to root<2>(m).
Traits¶
These traits provide information, at compile time, about the number represented by a Magnitude.
Is Integer?¶
Result: A bool indicating whether a Magnitude represents an integer (true if it does;
false otherwise).
Syntax:
- For a type
M:IsInteger<M>::value
- For an instance
m:is_integer(m)
Is Rational?¶
Result: A bool indicating whether a Magnitude represents a rational number (true if it
does; false otherwise).
Syntax:
- For a type
M:IsRational<M>::value
- For an instance
m:is_rational(m)
Is Positive?¶
Result: A bool indicating whether a Magnitude represents a positive number (true if it
does; false otherwise).
Syntax:
- For a type
M:IsPositive<M>::value
- For an instance
m:is_positive(m)
Integer part¶
Result: The integer part of a Magnitude, which is another Magnitude.
For example, the “integer part” of \frac{\sqrt{18}}{5\pi} would be 3, because \sqrt{27} = 3\sqrt{2}, and 3 is the integer part of 3\sqrt{2}.
If the input magnitude is an integer, then this operation is the identity.
If the input magnitude is not an integer, then this operation produces the largest integer factor that can be extracted from the numerator (that is, the base powers with positive exponent).1
Syntax:
- For a type
M:IntegerPartT<M>
- For an instance
m:integer_part(m)
Numerator (integer part)¶
Result: The numerator we would have if a Magnitude were written as a fraction. This result is
another Magnitude.
For example, the “numerator” of \frac{3\sqrt{3}}{5\pi} would be 3\sqrt{3}.
Syntax:
- For a type
M:NumeratorT<M>
- For an instance
m:numerator(m)
Denominator (integer part)¶
Result: The denominator we would have if a Magnitude were written as a fraction. This result is
another Magnitude.
For example, the “denominator” of \frac{3\sqrt{3}}{5\pi} would be 5\pi.
Syntax:
- For a type
M:DenominatorT<M>
- For an instance
m:denominator(m)
Absolute value¶
Result: The absolute value of a Magnitude, which is another Magnitude.
Syntax:
- For a type
M:Abs<M>
- For an instance
m:abs(m)
Sign¶
Result: A Magnitude: 1 if the input is positive, and -1 if the input is negative.
We expect that the relation m == sign(m) * abs(m) will hold for every Magnitude m.
Syntax:
- For a type
M:Sign<M>
- For an instance
m:sign(m)
-
The concept
integer_part()is conceptually ambiguous when applied to non-integers. So, too, fornumerator()anddenominator()applied to irrational numbers. These utilities serve two purposes. First, they provide a means for checking whether a given magnitude is a member of the unambiguous set — that is, we can check whether a magnitude is an integer by checking whether it’s equal to its “integer part”. Second, they enable us to automatically construct labels for magnitudes, by breaking them into the same kinds of pieces that a human reader would expect. ↩