Skip to content

Complete CODATA coverage with published uncertainties - #821

Merged
mpusz merged 10 commits into
masterfrom
codata-complete-coverage
Aug 6, 2026
Merged

Complete CODATA coverage with published uncertainties#821
mpusz merged 10 commits into
masterfrom
codata-complete-coverage

Conversation

@mpusz

@mpusz mpusz commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Generates the complete codata system from the NIST tables and lands everything that fell out of making that possible. Resolves #820.

What's here (7 commits, in dependency order)

  1. standard_uncertainty stores a constant's published absolute uncertainty — NIST publishes value and σ mutually rounded, so storing only the relative form reconstructed σ wrong (6.1% off for the fine-structure constant). A measured constant now declares exactly one of the two wrappers; accessors derive the other exactly; all 36 existing definitions migrate; also fixes hep::codata2022::electron_compton_wavelength wrongly aliasing the 2018 value.
  2. Compile-time factorization via double-wide mul_mod + batched-Brent Pollard's rho (approach from Fix compile-time factorization hitting constexpr step limit (#328) aurora-opensource/au#686, blessed by Chip Hogg) — a real CODATA mantissa (semiprime remainder 531'871 × 609'067) needed 265k trial-division iterations, over GCC's default constexpr loop limit; factorization drops from ~22s to ~0.9s of a CODATA TU.
  3. unit_magnitude interface hosted as hidden friends of a non-template base — a hidden friend of a class template is redeclared per specialization (6775 specializations × ~20 friends dominated compile time). Measured: codata2022.h TU 44.8s → 4.1s (gcc-15), si-baseline TU 2.6s → 1.6s — a library-wide win. Three-arm benchmark data recorded in the mp-units-benchmarks findings.
  4. The generated CODATA system — every row of all three adjustments accounted for (transcribed digit-for-digit / exact relation / alias / variant / curated skip; unaccounted rows fail generation), independently verified by re-evaluating every emitted expression against its source row. Two tiers per adjustment (_essential ≈ free, full ≈ +2.5s/TU) plus shared adopted_values.h and math_constants.h; iau.h includes only the essential tier.
  5. si::standard_gravity and si::reduced_planck_constant deprecated — neither is an SI defining constant; their homes are codata::standard_gravity (CGPM-adopted, adjustment-invariant) and codata::reduced_planck_constant (exact h/2π post-2019, measured in 2014). pound_force now builds on the codata entity via the tiny adopted-values header.
  6. Docs — both uncertainty forms across users guide/how-to/blog draft, the NIST row shown before the code that transcribes it, all quoted outputs regenerated from real code, and the systems reference covering all ~740 generated constants.

Verification

  • gcc-12-20, gcc-15-26, clang-16-20, clang-21-26: full builds green, 72/72 tests each (includes module builds)
  • generator self-verification: every emitted value and uncertainty recomputed in the row's own unit against the printed digits; --check reruns are byte-stable
  • scripts/systems_reference.py --force leaves the tree clean; full regeneration in ~20s
  • pre-commit clean on all touched files; NIST tables checked in byte-verbatim and excluded from whitespace-normalizing hooks

🤖 Generated with Claude Code

mpusz and others added 10 commits August 5, 2026 10:20
The post claimed `iau::G` and the HEP namespaces were the extent of the measured constants.
There is a `codata` system now, and IAU imports G from it rather than defining its own, so
the note pointed at a shape the library no longer has. Also links the tracking issue for
filling out the rest of the table.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ncertainty

NIST publishes a constant's value and its standard uncertainty mutually
rounded, both to two significant digits of sigma. Storing only the relative
form could not reproduce the published sigma (off by up to 6.1% for the
fine-structure constant), and sigma is the number `uncertain<T>` carries,
prints, and propagates. A measured constant now declares exactly one of two
wrappers, whichever form its source publishes:

- `standard_uncertainty{mag * unit}` - absolute, spelled as a full unit
  expression of the constant's own dimension, so the relative form derives
  as an exact ratio of canonical magnitudes with the units cancelling;
- `relative_standard_uncertainty{mag}` - retained for constants whose
  defining unit is not tabulated or needs an inexact conversion.

`MeasuredConstant` detects either wrapper, `get_standard_uncertainty` is
added, and both accessors derive the undeclared form on demand (using
`u_r = u(x)/|x|`, so negative-valued constants yield positive uncertainties
in both directions). The conversion engine reads `u_r` through the accessor
and needs no other change.

All 36 existing definitions (32 in `hep`, 4 in `codata`) migrate to the
absolute form, each transcribing its NIST table row verbatim, which removes
the derived-sigma error. This also surfaced that
`hep::codata2022::electron_compton_wavelength` wrongly aliased the 2018
value: CODATA 2022 publishes 2.426 310 235 38(76)e-12 m, so it is now a
definition of its own.

`scripts/systems_reference.py` renders whichever form a constant declares
in a single "Standard uncertainty" column, and the `~` format-spec rationale
is reworded: it quotes the value to the precision the uncertainty justifies
(GUM 7.2.6) rather than marking the output as an approximation.

Part of #820

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d-Brent Pollard's rho

`find_first_factor` relied on unbounded odd trial division after the
Baillie-PSW primality gate. BPSW certifies large primes cheaply, but for a
composite whose prime factors all exceed the trial-division range it only
answers "composite" and the loop then walks to the smallest factor: the
CODATA 2018 atomic unit of electric potential mantissa
(27'211'386'245'988 = 2^2 * 3 * 7 * 531'871 * 609'067) needs 265'665
iterations, over GCC's default `-fconstexpr-loop-limit` of 262'144, so the
value could not be defined at all. The 2018 electron Compton wavelength
already shipped at 89% of that ceiling.

Following the approach validated in Au (aurora-opensource/au#686, blessed by
Chip Hogg for porting): `mul_mod` now forms the full-width product in
`unsigned __int128` where the compiler provides one (the original chunking
reduction stays as the portable fallback), and factors are found by Pollard's
rho with Brent's cycle detection and batched gcd checks, recursing with BPSW
to return the smallest prime factor. Trial division remains as the last-resort
fallback. Factorization drops from ~22 s to ~0.9 s of a CODATA-heavy TU, and
the previously impossible value compiles in about a second.

Free functions are used instead of local lambdas on purpose: only C++23
consteval propagation lets a plain lambda call `consteval` functions, and
gcc-12 builds in C++20 mode.

Part of #820

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…f a non-template base

A hidden friend of a class template is redeclared by every specialization,
and magnitude-heavy code instantiates thousands of specializations per
translation unit (every product materializes intermediates), so the ~20
friends `unit_magnitude` carried made its instantiations dominate compile
times: 30 s of a 42 s CODATA translation unit went into `InstantiateClass`
over 6775 specializations.

Everything taking a magnitude now lives in the new non-template
`unit_magnitude_interface` base, the same shape as `unit_interface` and
`quantity_spec_interface`: declared exactly once per program, found through
ADL exactly as before (a base class is an associated class of the argument),
invisible to ordinary lookup, and with nothing of these names left at
namespace scope an ADL call on a magnitude sees exactly one candidate family.
Only `empty_magnitude` and `negate_magnitude` stay outside, because a friend
body cannot name a concrete specialization of the still-incomplete class
template, plus the element-level utilities that take no magnitude at all.
The `magnitude_base` CRTP layer is gone, and all magnitude parameters are
taken by value.

Measured on the three-arm benchmark (best-of-3, interleaved; full data in the
mp-units-benchmarks findings):

| TU                        | before  | after  |
|---------------------------|---------|--------|
| codata2022.h, gcc-15      | 44.8 s  | 4.1 s  |
| codata2022.h, clang-21    | 44.0 s  | 6.8 s  |
| si units + constants TU   | 2.6 s   | 1.6 s  |

The last row applies to every mp-units user: the win is library-wide, not
CODATA-specific.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`scripts/codata_constants.py` generates the `codata` system headers from the
NIST "allascii" tables (checked in byte-verbatim under `scripts/codata/` and
excluded from whitespace-normalizing hooks). Every row of every adjustment is
accounted for in exactly one way - transcribed with its published value and
absolute standard uncertainty digit for digit, defined by a curated exact
relation (values NIST prints truncated with "..." are exact non-terminating
decimals), aliased (`si::si2019` constants and exact-duplicate rows), skipped
as a unit variant, or skipped for a curated documented reason - and an
unaccounted row fails generation. The output is then verified independently:
every emitted expression is re-parsed and re-evaluated numerically in the
row's own unit against the printed digits.

Each adjustment ships in two tiers spelling the same types:
`codata/codataYYYY_essential.h` (the NIST "Frequently used constants"
selection, costing nothing measurable over the framework headers) and
`codata/codataYYYY.h` (the complete table, ~230 constants). Adjustment-
invariant values live outside the tiers: `codata/adopted_values.h` holds the
CGPM/CIPM conventional constants that are identical in every table (the
generator verifies that), and `codata/math_constants.h` defines the two Wien
displacement transcendental roots that the post-2019 Wien constants are
defined through. `codata.h` remains the umbrella, and `iau.h` now includes
only the essential tier.

`scripts/systems_reference.py` learns the new layout: per-constant-alias
inline-namespace detection (fixing triplicated index entries with dead
anchors), alias target resolution that prefers a nearer system's constant
over a farther system's quantity, symbol cells with word-break opportunities,
and uncertainty carried through constant aliases.

Resolves #820

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…precated in favor of their `codata` homes

Neither is an SI defining constant; both sat in `si` for convenience. The
standard gravity is a conventional value adopted by CGPM 3 (1901), which is
exactly what NIST's "adopted values" CODATA category holds, so its home is
`codata::standard_gravity` in `<mp-units/systems/codata/adopted_values.h>`,
a single adjustment-invariant entity. The reduced Planck constant's home is
`codata::reduced_planck_constant`: the exact `h / 2π` relation in the
post-2019 adjustments and the measured "Planck constant over 2 pi" value in
`codata2014`. The deprecated `si` duplicates stay for compatibility,
following the `si::magnetic_constant` precedent.

`yard_pound`'s `pound_force` is now defined through the codata entity and
includes only the tiny adopted-values header. All in-repo users (examples,
tests, tutorials, workshops) migrate; the docs snippets gain the include,
and the `weighing_the_earth` example page's snippet pins are shifted for the
added include line. A constant should have exactly one home so that every
alias refers to the very same entity and conversions through it stay exact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…generated reference

The users guide, the how-to guide, and the blog draft now describe the two
uncertainty wrappers the way the design settled: `standard_uncertainty`
transcribes the published absolute value verbatim (NIST rounds the pair
mutually, so no derived form reproduces it), and
`relative_standard_uncertainty` remains for sources publishing only the
relative form, with each accessor deriving the other on demand. The blog
post shows the NIST table row before the code that transcribes it, and the
codata users-guide page documents the full header layout (tiers, adopted
values, Wien math constants) with current compile-time numbers.

Every output quoted in the docs is regenerated from the real code: the
weighing-the-earth accepted-value sigma is the published one now, and the
systems reference covers all ~740 generated constants.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The MD013 fix on the neighboring line pushed words down and made this one
97 characters. Verified with a full `pre-commit run --all-files` (exit code
checked, not eyeballed output) before pushing this time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… forks

Seven workflows carried both a `push` trigger over all branches and a
`pull_request` trigger, so every push to a branch with an open PR started
two runs of each. They land in different concurrency groups - `github.ref`
is `refs/heads/<branch>` for one and `refs/pull/N/merge` for the other - so
neither cancels the other and they compete for runners.

The two events also filter paths differently, and that is what made the
duplication expensive rather than merely redundant: `push` matches the
paths against the commits just pushed, while `pull_request` matches them
against the whole base...head diff. Once any commit in a PR has touched
`src/**`, every later push re-runs the full matrix, even a docs-only one.

Skip the `pull_request` event where `push` already covers it - that is,
whenever the head branch lives in this repository. Fork PRs never fire
`push` here, so they keep full `pull_request` coverage on every push, and
comparing `head.repo.full_name` rather than the repository name keeps that
working for forks renamed away from `mp-units`.

The guard sits on the first job of each workflow; the rest are chained
through `needs:` and skip with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sion

All three failing MSVC 14.5 CI legs die identically in
`prime_factorization::first_base` with a bogus "failure was caused by a read
of an uninitialized symbol: std::_Optional_destruct_base<uint64_t>::_Has_value"
the moment `find_first_factor` reaches the Pollard's rho path (first hit:
the codata2014 alpha particle-proton mass ratio mantissa). GCC and Clang
evaluate the same code fine.

`smallest_prime_factor_of_hard_composite` and `smallest_prime_in` now return
a plain `std::uint64_t` with `0` (never a valid prime factor) as the
every-parameterization-failed sentinel instead of `std::optional`. Verified
on gcc-15 (full build, all tests) and covered by the existing
`find_first_factor` static assertions, which exercise the rho path directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mpusz
mpusz merged commit a4ff2a4 into master Aug 6, 2026
120 checks passed
@mpusz
mpusz deleted the codata-complete-coverage branch August 6, 2026 12:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Complete the CODATA system by generating it from the NIST tables

1 participant