Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci-clang-tidy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ concurrency:
jobs:
generate-matrix:
name: "Generate build matrix for ${{ github.workflow }}"
# A PR whose head branch lives in this repository is already covered by the `push` event above,
# whose path filters match only the commits just pushed - unlike `pull_request`, which matches
# them against the whole base...head diff and so re-runs everything for a docs-only push. Fork
# PRs never fire `push` here, so they are what this event exists to serve.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
seed: ${{ steps.set-matrix.outputs.seed }}
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/ci-compile-time.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ env:

jobs:
instantiations:
# A PR whose head branch lives in this repository is already covered by the `push` event above,
# whose path filters match only the commits just pushed - unlike `pull_request`, which matches
# them against the whole base...head diff and so re-runs everything for a docs-only push. Fork
# PRs never fire `push` here, so they are what this event exists to serve.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository
# One gate per supported language standard: counts differ per -std, and a regression can appear
# in one standard alone. Each has its own reviewed baselines in the benchmarks repository.
strategy:
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/ci-conan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ concurrency:
jobs:
generate-matrix:
name: "Generate build matrix for ${{ github.workflow }}"
# A PR whose head branch lives in this repository is already covered by the `push` event above,
# whose path filters match only the commits just pushed - unlike `pull_request`, which matches
# them against the whole base...head diff and so re-runs everything for a docs-only push. Fork
# PRs never fire `push` here, so they are what this event exists to serve.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
seed: ${{ steps.set-matrix.outputs.seed }}
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci-formatting.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ concurrency:

jobs:
check:
# A PR whose head branch lives in this repository is already covered by the `push` event above.
# Fork PRs never fire `push` here, so they are what this event exists to serve.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/ci-freestanding.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ concurrency:
jobs:
generate-matrix:
name: "Generate build matrix for ${{ github.workflow }}"
# A PR whose head branch lives in this repository is already covered by the `push` event above,
# whose path filters match only the commits just pushed - unlike `pull_request`, which matches
# them against the whole base...head diff and so re-runs everything for a docs-only push. Fork
# PRs never fire `push` here, so they are what this event exists to serve.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
seed: ${{ steps.set-matrix.outputs.seed }}
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/ci-test-package-cmake.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ concurrency:
jobs:
generate-matrix:
name: "Generate build matrix for ${{ github.workflow }}"
# A PR whose head branch lives in this repository is already covered by the `push` event above,
# whose path filters match only the commits just pushed - unlike `pull_request`, which matches
# them against the whole base...head diff and so re-runs everything for a docs-only push. Fork
# PRs never fire `push` here, so they are what this event exists to serve.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
seed: ${{ steps.set-matrix.outputs.seed }}
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/citation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ concurrency:

jobs:
validate-citation-cff:
# A PR whose head branch lives in this repository is already covered by the `push` event above.
# Fork PRs never fire `push` here, so they are what this event exists to serve.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-latest
timeout-minutes: 10
name: Validate CITATION.cff
Expand Down
3 changes: 3 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ repos:
hooks:
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
# the NIST CODATA tables are checked in byte-verbatim; nothing may rewrite them
exclude: ^scripts/codata/
- id: end-of-file-fixer
exclude: ^scripts/codata/
- id: check-yaml
args: [--unsafe] # mkdocs.yml uses !ENV and !!python/name: tags
- id: check-json
Expand Down
57 changes: 48 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,51 @@ This page documents the version history and changes for the **mp-units** library
- fix: `uncertain<T>::relative_uncertainty()` now divides by the absolute value. GUM and
the VIM define the relative standard uncertainty as `u(x)/|x|`, so it is never
negative; it used to come out negative for a negative central value
- feat: compile-time factorization now handles composites of large primes: `mul_mod`
uses a double-wide (128-bit) product where the compiler provides one, and
`find_first_factor` finds factors with a batched-Brent Pollard's rho instead of
unbounded trial division (which needed ~266k `constexpr` iterations for real CODATA
mantissas, over GCC's default loop limit). Mirrors the approach validated in Au
(aurora-opensource/au#686); trial division remains as the last-resort fallback
- perf: the whole `unit_magnitude` interface (operators and all queries) moved to hidden
friends of a new non-template `unit_magnitude_interface` base, the same shape as
`unit_interface` and `quantity_spec_interface`. A hidden friend of a class template
is redeclared by every specialization, and magnitude-heavy code instantiates
thousands of specializations per translation unit, so those declarations dominated
compile times; in a non-template base they are declared once and cost nothing while
keeping all hidden-friend benefits. A TU including `si` units and constants dropped
from ~2.6 s to ~1.6 s, and the complete `codata2022.h` from ~40 s to ~4.1 s
(GCC 15; Clang 21 shows 44 s -> 6.8 s)
- (!) feat: `pi` and `π` is now a unit constant
- (!) feat: `iau::newtonian_constant_of_gravitation` is now imported from
`codata::newtonian_constant_of_gravitation` instead of being defined by the IAU system,
and the `iau::codata2018` inline namespace is gone. IAU 2015 Resolution B3 asks for
the current best estimate of G, so the value now follows whichever adjustment is
current. The value is unchanged today, since CODATA 2022 kept G at 6.674 30(15)e-11
- feat: CODATA fundamental physical constants added as a separate system in
`mp-units/systems/codata.h` (`codata::codata2014`, `codata::codata2018`, and the
`inline` `codata::codata2022`). Deliberately not part of `si`, so that
`mp-units/systems/si.h` does not carry constants most code never names
- feat: CODATA fundamental physical constants added as a separate system with one
namespace and one header per adjustment (`codata::codata2014`, `codata::codata2018`,
and the `inline` `codata::codata2022`), GENERATED from the NIST "allascii" tables by
`scripts/codata_constants.py` for complete coverage (~230 constants per adjustment):
measured constants transcribe the published value and absolute standard uncertainty
digit for digit, exact non-terminating values are defined by their exact symbolic
relations, and an independent verification step recomputes every emitted value
against its source row. Each adjustment ships in two tiers:
`mp-units/systems/codata/codataYYYY_essential.h` (the NIST "frequently used"
selection, cheap to compile) and `mp-units/systems/codata/codataYYYY.h` (the complete
table, including the essential tier), with `mp-units/systems/codata.h` as the
umbrella. Deliberately not part of `si`, so that `mp-units/systems/si.h` does not
carry constants most code never names (#820)
- deprecated: `si::standard_gravity`. It is not an SI defining constant but a conventional
value adopted by CGPM 3 (1901), which is what NIST's "adopted values" CODATA category
holds; its home is now `codata::standard_gravity` from
`<mp-units/systems/codata/adopted_values.h>`, a single adjustment-invariant entity
shared by all CODATA namespaces (as are the standard atmosphere, the standard-state
pressure, and the conventional Josephson and von Klitzing values). `yard_pound`'s
`pound_force` is now defined through the codata entity
- deprecated: `si::reduced_planck_constant`. ℏ is not an SI defining constant either; its
home is `codata::reduced_planck_constant`, defined as the exact `h / 2π` relation in
the post-2019 adjustments and as the measured "Planck constant over 2 pi" in
`codata2014`
- deprecated: `si::magnetic_constant`. The pre-2019 SI defined the ampere through it,
which made it exactly 4*pi*1e-7 H/m; since the redefinition it is measured. Its
value is unchanged, but use `codata::magnetic_constant` for the current adjustment,
Expand Down Expand Up @@ -54,11 +89,15 @@ This page documents the version history and changes for the **mp-units** library
- (!) feat: IAU system definition improved
- feat: `uncertain<T>` representation type added to `mp-units::utility` (first-order
uncertainty propagation for independent values) (#464)
- feat: measured constants may now declare their `relative_standard_uncertainty` (exact
magnitude metadata on `named_constant`); `MeasuredConstant` concept,
`get_relative_standard_uncertainty`, and `measurement_of` added; declared for
`iau::newtonian_constant_of_gravitation` and all measured constants in the three
`hep` CODATA namespaces (#464)
- feat: measured constants may now declare their standard uncertainty in the form their
source publishes it (exact metadata on `named_constant`): `standard_uncertainty`
(absolute, in the constant's own dimension, transcribing the CODATA table row
verbatim) or `relative_standard_uncertainty` (unit-invariant, for constants whose
defining unit is not tabulated); `MeasuredConstant` concept,
`get_standard_uncertainty`, `get_relative_standard_uncertainty`, and
`measurement_of` added, with each accessor deriving the undeclared form on demand;
declared for `iau::newtonian_constant_of_gravitation` and all measured constants in
the three `hep` CODATA namespaces (#464, #820)
- feat: unit conversion factors built from measured constants carry their relative standard
uncertainty; the conversion engine folds it into representation types that opt in
via `fold_conversion_uncertainty` (e.g., `uncertain<T>`), with measured constants
Expand Down
76 changes: 53 additions & 23 deletions docs/blog/posts/measurement-uncertainty-and-measured-constants.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
---
date: 2026-08-05
date: 2026-08-06
authors:
- mpusz
categories:
- Metrology
comments: true
draft: true
---

# Measurement uncertainty and measured constants
Expand All @@ -20,8 +19,8 @@ happily prints ten significant digits of a solar mass computed from a constant t
guarantees four.

**mp-units** now models this honestly. This post introduces the `uncertain<T>` representation
type, the `relative_standard_uncertainty` metadata for measured constants, and the
`measurement_of` helper that connects the two.
type, the `standard_uncertainty` and `relative_standard_uncertainty` metadata for measured
constants, and the `measurement_of` helper that connects them.

<!-- more -->

Expand Down Expand Up @@ -116,20 +115,42 @@ adjective-wrapper naming pattern as `std::optional<T>` and `std::expected<T>`.

## Measured constants carry their uncertainty

A measured constant now declares its relative standard uncertainty right in its definition,
as an exact symbolic magnitude, using the exact term the CODATA table uses:
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](https://physics.nist.gov/cuu/Constants/ArchiveASCII/allascii_2018.txt),
with a quantity name, a value, a standard uncertainty, and a unit:

```text
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:

```cpp
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),
relative_standard_uncertainty{mag_ratio<22, 10> * mag_power<10, -5>}> {}
standard_uncertainty{mag_ratio<15, 10> * mag_power<10, -15> * cubic(si::metre) / si::kilogram / square(si::second)}> {}
newtonian_constant_of_gravitation;
```

The definition reads like the CODATA entry it transcribes: the value `6.674 30 × 10⁻¹¹`, and
the relative standard uncertainty `2.2 × 10⁻⁵`. 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.
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.
Expand Down Expand Up @@ -157,8 +178,8 @@ 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.749e+25 kg
std::cout << value_cast<kg, uncertain<double>>(two_suns) << "\n"; // 3.97682e+30 ± 8.749e+25 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
Expand All @@ -175,7 +196,7 @@ 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.749e+25 kg
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
Expand Down Expand Up @@ -211,8 +232,8 @@ std::cout << "M_sun = " << solar_mass.in(kg) << "\n";
```

```text
G = 6.6743e-11 ± 1.46835e-15 m³ kg⁻¹ s⁻²
M_sun = 1.98841e+30 ± 4.3745e+25 kg
G = 6.6743e-11 ± 1.5e-15 m³ kg⁻¹ s⁻²
M_sun = 1.98841e+30 ± 4.46881e+25 kg
```

The solar mass example is the payoff. The IAU defines the nominal solar mass parameter
Expand Down Expand Up @@ -249,23 +270,29 @@ both units is fully correlated with itself, and it cancels symbolically before t
is ever applied, which is exactly why the solar-to-Earth-mass conversion reports zero rather
than a small non-zero number.

**Coverage.** `iau::G` and every measured constant in the three HEP CODATA namespaces carry
the uncertainty published by their own release. 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.
**Coverage.** Every measured constant in the three HEP CODATA namespaces carries the
uncertainty published by its own release. The new
[`codata`](../../users_guide/systems/codata.md) 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:

```cpp
std::cout << G.in(m3 / kg / s2) << "\n"; // 6.6743e-11 ± 1.46835e-15 m³ kg⁻¹ s⁻²
std::cout << G.in(m3 / kg / s2) << "\n"; // 6.6743e-11 ± 1.5e-15 m³ kg⁻¹ s⁻²
```

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. The tilde marks the
output as an approximation, because unlike the default form this one rounds the value:
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:

```cpp
std::println("{::N[~]}", G.in(m3 / kg / s2)); // 6.67430(15)e-11 m³ kg⁻¹ s⁻²
Expand Down Expand Up @@ -303,6 +330,9 @@ documentation:
it: the mass of the planet from a pendulum, a radius, and `G`
- [Faster-than-lightspeed constants](../../users_guide/framework_basics/faster_than_lightspeed_constants.md)
explains why constants are units in the first place
- [The CODATA system](../../users_guide/systems/codata.md) documents the generated
constants: one namespace and one header per adjustment, and the essential and complete
tiers
- [CODATA fundamental physical constants](https://physics.nist.gov/cuu/Constants/) are the
source of the values and uncertainties transcribed into the systems
- [JCGM 100 (GUM)](https://www.bipm.org/en/committees/jc/jcgm/publications) is the
Expand Down
Loading
Loading