Measurement uncertainty and measured constants¶
When you write constexpr double G = 6.674e-11; in your code, the type system makes a claim
that is not true. It claims the value is exact. It is not. The Newtonian constant of
gravitation is one of the least precisely known constants in physics. CODATA 2018 lists it
as 6.674 30(15) × 10⁻¹¹ m³ kg⁻¹ s⁻², and the (15) means the fifth significant digit is
already uncertain. Only four digits of G are actually known. Every result derived from
that double inherits an uncertainty that the program neither tracks nor reports. The code
happily prints ten significant digits of a solar mass computed from a constant that
guarantees four.
mp-units now models this honestly. This post introduces the uncertain<T> representation
type, the standard_uncertainty and relative_standard_uncertainty metadata for measured
constants, and the measurement_of helper that connects them.
Constants are units, and that is not going to change¶
Since the beginning of the V2 design, physical constants in mp-units are units. This is the Faster-than-lightspeed constants feature. The central value of a constant lives in the unit's exact symbolic magnitude, so:
- constants cancel symbolically with zero runtime cost and zero precision loss,
- no representation type is forced on the user (
1 * Gworks withint,double, or anything else), - the conversion cost is paid only when (and if) the user asks for a specific unit.
It turns out this design also matches metrological practice. The IAU 2015 Resolution B3 nominal solar and planetary constants are exact by definition precisely so that they can serve as conversion units. The seven defining constants of the 2019 SI are exact as well. For all of those, an exact unit is not an approximation of the truth. It is the truth.
The gap was elsewhere. Some constants are measured, not defined. G, the particle masses
in the HEP system, the fine-structure constant. For those, the CODATA table publishes two
numbers: the central value and the relative standard uncertainty. The unit magnitude stores
the first one. The second one had nowhere to live. This gap was first reported in
#464, and
Ralph Steinhagen explored a full uncertainty-propagation
type in that discussion back in 2023.
Where should uncertainty live?¶
We considered three options.
Inside unit magnitudes? No. Unit equality compares canonical magnitudes, and a conversion factor is a ratio of them, so both are arithmetic on magnitudes and both depend on the operands being exact numbers. A "unit of G ± σ" could not produce a well-defined conversion factor or a decidable equality. This is not a limitation to lift someday. Exactness is what makes constants-as-units work at all.
Constants as quantities? Also no. A quantity constant like
inline constexpr quantity G = 6.674e-11 * si::metre * ...; forces a representation type
on every user. A float codebase would silently promote to double. It also gives up exact
cancellation.
Declared in the constant, tracked by the representation. This is the one, and it is not a single place. A measured constant's definition carries both numbers CODATA publishes: the central value becomes the unit's exact symbolic magnitude, and the relative standard uncertainty sits right next to it as metadata. The magnitude stays exact, which is what keeps unit equality and conversion-factor arithmetic working. The constant does not claim to be exact anymore: its definition now states openly that the value is measured, and how well. Tracking is a separate job with a separate home. A dedicated representation type carries uncertainty through arithmetic, and the conversion engine materializes the declared one whenever a conversion factor depends on the constant. Each number lands in the layer that can handle it correctly.
uncertain<T>¶
The mp_units::utility component now provides uncertain<T>, a representation type that
pairs a central value with its standard uncertainty and propagates uncertainty through
arithmetic using the standard first-order formulas:
#include <mp-units/systems/si.h>
#include <mp-units/utility/uncertain.h>
using namespace mp_units;
using namespace mp_units::si::unit_symbols;
using mp_units::utility::uncertain;
quantity length = uncertain{10.0, 0.1} * m; // 10.0 ± 0.1 m
quantity width = uncertain{5.0, 0.05} * m; // 5.0 ± 0.05 m
quantity area = length * width; // 50.0 ± 0.71 m²
No customization is needed. uncertain<T> simply satisfies the RepresentationOf concept,
so it works with every quantity, every unit conversion, and every math function that its
underlying type supports. An exact quantity joins the propagation implicitly as value ± 0,
so mixing uncertain and plain representations in one expression works as expected.
The name follows the VIM (International Vocabulary of Metrology) terminology on purpose.
A measurement is the process, and a measurement result is a value with an uncertainty
and a unit. A bare {value, σ} pair with no unit attached is neither. It is just an
uncertain number, so the type is called uncertain, and it follows the same
adjective-wrapper naming pattern as std::optional<T> and std::expected<T>.
Scope
uncertain<T> implements first-order propagation for statistically independent values.
It does not track correlations, so x - x reports a non-zero uncertainty. Full rigor
for correlated values requires covariance matrices, and Ralph's prototype in
#464 shows how quickly that grows.
We chose the 80/20 boundary and documented it. If your problem needs correlation
tracking, a dedicated uncertainty library is the right tool.
Measured constants carry their uncertainty¶
A measured constant now declares its standard uncertainty right in its definition, as an
exact symbolic expression, using the exact term the CODATA table uses. This is how NIST
publishes G in the machine-readable
CODATA 2018 table,
with a quantity name, a value, a standard uncertainty, and a unit:
Quantity Value Uncertainty Unit
-----------------------------------------------------------------------------
Newtonian constant of gravitation 6.674 30 e-11 0.000 15 e-11 m^3 kg^-1 s^-2
and this is how that row splits into code:
inline constexpr struct newtonian_constant_of_gravitation final :
named_constant<"G", mag_ratio<667'430, 100'000> * mag_power<10, -11> * cubic(si::metre) / si::kilogram / square(si::second),
standard_uncertainty{mag_ratio<15, 10> * mag_power<10, -15> * cubic(si::metre) / si::kilogram / square(si::second)}> {}
newtonian_constant_of_gravitation;
Every column of the row lands in the definition: the value 6.674 30 × 10⁻¹¹ and the
standard uncertainty 0.000 15 × 10⁻¹¹ digit for digit, both in the constant's own unit
m³ kg⁻¹ s⁻². Transcribing the pair verbatim matters more than it may look. NIST
publishes the value and the uncertainty mutually rounded, each to two significant digits
of the uncertainty, so no derived form reproduces them exactly. Storing the relative
uncertainty instead (an earlier iteration of this design) reconstructed a σ that was off by
6.1% for the fine-structure constant. σ is the number uncertain<T> carries, prints, and
propagates, so it is the wrong number to get wrong.
A second wrapper, relative_standard_uncertainty, remains for constants whose source
publishes only the relative form. A measured constant declares exactly one of the two, and
the accessors derive the missing form on demand as an exact ratio of canonical magnitudes.
No representation type appears anywhere. The metadata is an annotation only. It never
participates in unit equality, conversion factors, or symbolic simplification, so nothing
about the existing constant behavior changes.
Exact constants simply do not have this parameter. This makes exact-vs-measured a
distinction the type system can see, expressed by the new MeasuredConstant concept.
Previously that distinction lived only in comments.
Conversions materialize the uncertainty¶
Working through the design surfaced a fact that shaped everything else: the uncertainty of
a measured constant is relational. In its own unit the constant is exactly 1. "Two
solar masses" is an exact statement even though iau::solar_mass is defined through G,
and it stays exact in an uncertainty-capable representation too: uncertain<double>{2.0} *
iau::unit_symbols::M_SUN prints 2 ± 0 M_☉. What is uncertain is the conversion factor
between
such a unit and others: how many kilograms a solar mass really is depends on how well G
is known.
This is why the conversion engine is the place where a constant's declared uncertainty becomes part of a value. When a conversion's factor is built from measured constants and the representation type can store an uncertainty, the factor's relative standard uncertainty is folded into the result in quadrature. For every other representation type nothing changes:
const quantity two_suns = 2.0 * iau::unit_symbols::M_SUN;
std::cout << two_suns << "\n"; // 2 M_☉
std::cout << two_suns.in(kg) << "\n"; // 3.97682e+30 kg
std::cout << two_suns.in<uncertain<double>>(kg) << "\n"; // 3.97682e+30 ± 8.93761e+25 kg
std::cout << value_cast<kg, uncertain<double>>(two_suns) << "\n"; // 3.97682e+30 ± 8.93761e+25 kg
The first two lines are exact: two_suns in its own unit, and the sanctioned central
value in kilograms. The third opts into the uncertainty at the conversion, and the
fourth spells the same opt-in as a cast.
The derivation walks both units' definitions and accumulates the net exponent of every measured constant, so a constant contributing to both sides cancels symbolically, exactly as its central value cancels in the magnitude ratio. Here are the same two suns again, with the uncertainty-capable representation as the only difference:
const quantity two_suns = uncertain<double>{2.0} * iau::unit_symbols::M_SUN;
std::cout << two_suns << "\n"; // 2 ± 0 M_☉
std::cout << two_suns.in(iau::unit_symbols::M_EARTH) << "\n"; // 665892 ± 0 M_⊕
std::cout << two_suns.in(kg) << "\n"; // 3.97682e+30 ± 8.93761e+25 kg
two_suns is exactly two suns even in an uncertainty-capable representation. Both masses
are defined as (GM)ᴺ/G, so G cancels in the conversion to Earth masses and the reported
uncertainty stays exactly zero there too. Only in kilograms does it survive, and since the
representation type can hold it, no explicit opt-in is needed anymore.
That cancellation is something no value-level scheme can get right. Once an uncertainty is baked into a number, propagating it through a solar-to-Earth-mass conversion double-counts what the symbolic form knows how to cancel.
measurement_of¶
measurement_of is a small factory function built on the same rule. Its argument is
constrained to the MeasuredConstant concept, so it accepts exactly the constants that
declare a relative standard uncertainty and rejects everything else at compile time. For
such a constant it returns the quantity 1 in an uncertainty-capable representation, and
the uncertainty appears the moment the quantity leaves units in which the constant
cancels. The underlying numeric type defaults to double, consistent with the rest of the
library, and any other supported one can be selected explicitly, so a float codebase
never touches a double:
const quantity G_measured = measurement_of(iau::newtonian_constant_of_gravitation);
const quantity G_single = measurement_of<float>(iau::newtonian_constant_of_gravitation);
static_assert(std::is_same_v<decltype(G_measured)::rep, uncertain<double>>);
static_assert(std::is_same_v<decltype(G_single)::rep, uncertain<float>>);
std::cout << "G = " << G_measured.in(m3 / kg / s2) << "\n";
const quantity solar_mass = (1. * iau::nominal_solar_mass_parameter) / G_measured;
std::cout << "M_sun = " << solar_mass.in(kg) << "\n";
The solar mass example is the payoff. The IAU defines the nominal solar mass parameter
(GM)☉ᴺ as exact, so the entire uncertainty of the derived solar mass comes from G, and
the result correctly reports it: 1.98841 × 10³⁰ kg, known precisely as well as G itself
and no better, which is what the IAU intended when it switched to nominal values in 2015.
This is where that MeasuredConstant constraint earns its keep. Asking for the measurement
of a constant that is exact by definition does not compile:
The rationale behind this is that:
- "exact by definition" and "measured infinitely precisely" are different metrological
statements, and returning
±0would conflate them, - asking for the measurement of a defined constant is a category error, and we prefer to break at compile time, not at runtime.
Limits¶
Cross-constant independence is assumed. The quadrature sum u_r = √(Σ (nᵢ · u_r,ᵢ)²)
treats the measured constants surviving in a conversion factor as independent. CODATA
publishes covariances between its values, and modeling them is deliberately out of scope.
It is the same boundary uncertain<T> itself draws, for the same reason: doing correlation
properly means carrying a covariance matrix, and that is a different library. When several
correlated constants do survive into one factor, the reported uncertainty can err in either
direction, depending on the signs of their exponents and of the correlation between them.
The case where correlation matters most is not approximated at all. A constant appearing in both units is fully correlated with itself, and it cancels symbolically before the formula is ever applied, which is exactly why the solar-to-Earth-mass conversion reports zero rather than a small non-zero number.
Coverage. Every measured constant in the three HEP CODATA namespaces carries the
uncertainty published by its own release. The new
codata system goes further: its headers are
generated from the NIST tables and cover every constant of the 2014, 2018, and 2022
adjustments (roughly 230 per adjustment), each transcribing the published value and
standard uncertainty digit for digit, with an independent verification step recomputing
every emitted value against its source row. iau::G now imports the CODATA constant
rather than duplicating it. Constants that are exact by definition carry nothing at all,
which is what makes the distinction visible to the type system in the first place.
Printing what the standard prints¶
The default text output is the value ± σ form, the notation engineers read every day:
ISO 80000-1:2022, 7.2.4 specifies a different one. The value is quoted to the last
significant digit of the uncertainty, and the uncertainty follows in parentheses, counted
in units of that digit. Appending ~ to the format spec selects it. Where the default form
prints what the object holds, this one quotes the value to the precision the uncertainty
justifies, which is how measurement results are reported:
That is character for character how CODATA publishes the value, which is the point. A constant transcribed from a published table now prints back in the notation of the table it came from.
The standard is also not neutral between the two forms:
ISO 80000-1:2022, 7.2.4
Uncertainties are often expressed in the following manner: \((23{,}478\,2 \pm 0{,}003\,2)\ \mathrm{m}\). This is, however, wrong from a mathematical point of view. [...] "The ± format should be avoided whenever possible because it has traditionally been used to indicate an interval corresponding to a high level of confidence and thus may be confused with expanded uncertainty".
The ± form nevertheless remains the default, because the concise form has to round the
value to the precision of the uncertainty, and a default that silently discards digits is
the wrong default. The recommendation applies to published results, and that is exactly
where reaching for ~ costs one character.
Further Reading¶
This post covers the design and the reasoning behind it. The working details live in the documentation:
- Working with Measurement Uncertainty
is the how-to guide: the observers
uncertain<T>exposes, the full table of propagation formulas including the trigonometric ones, what its ordering does and does not mean, which element types it accepts, and the measured-constant workflow step by step - Weighing the Earth is a complete program using
it: the mass of the planet from a pendulum, a radius, and
G - Faster-than-lightspeed constants explains why constants are units in the first place
- The CODATA system documents the generated constants: one namespace and one header per adjustment, and the essential and complete tiers
- CODATA fundamental physical constants are the source of the values and uncertainties transcribed into the systems
- JCGM 100 (GUM) is the authority on how uncertainty is expressed and propagated
Acknowledgments¶
Credits go to Ralph Steinhagen for the prototype and the discussion that shaped the scope, and to the ISO/IEC 80000 and CODATA tables for having the answer ready every time we wondered what the right behavior should be.
Please share your feedback in the comments below.