From 8fbe009d5861699524657d976d8f18c73b3a2d55 Mon Sep 17 00:00:00 2001 From: Mateusz Pusz Date: Wed, 5 Aug 2026 10:20:09 +0200 Subject: [PATCH 01/10] docs(blog): the coverage note predates the CODATA system 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) --- .../measurement-uncertainty-and-measured-constants.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/blog/posts/measurement-uncertainty-and-measured-constants.md b/docs/blog/posts/measurement-uncertainty-and-measured-constants.md index aa073e6649..a9710322b8 100644 --- a/docs/blog/posts/measurement-uncertainty-and-measured-constants.md +++ b/docs/blog/posts/measurement-uncertainty-and-measured-constants.md @@ -249,10 +249,13 @@ 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, as do the SI-expressed ones in the new +[`codata`](../../users_guide/systems/codata.md) system, which `iau::G` now imports rather +than duplicating. 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. Filling out the +rest of the CODATA table is tracked in +[#820](https://github.com/mpusz/mp-units/issues/820). ## Printing what the standard prints From d7fd2f8ef58b9996f4203ad97743c7b905d7f150 Mon Sep 17 00:00:00 2001 From: Mateusz Pusz Date: Wed, 5 Aug 2026 15:32:50 +0200 Subject: [PATCH 02/10] feat: `standard_uncertainty` stores a constant's published absolute uncertainty 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` 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 --- CHANGELOG.md | 14 +- docs/reference/systems_reference/.cache.json | 2 +- .../systems_reference/systems/codata.md | 14 +- .../systems_reference/systems/hep.md | 72 +++++----- .../systems_reference/systems/iau.md | 8 +- scripts/systems_reference.py | 125 ++++++++++++------ .../mp-units/bits/unit_conversion_impl.h | 8 +- .../mp-units/framework/unit_concepts.h | 8 +- .../mp-units/framework/unit_definitions.h | 98 +++++++++++++- src/systems/include/mp-units/systems/codata.h | 14 +- .../include/mp-units/systems/hep/constants.h | 125 ++++++++++-------- .../include/mp-units/utility/uncertain.h | 7 +- test/runtime/uncertain_test.cpp | 27 ++-- test/static/uncertain_test.cpp | 113 ++++++++++++---- 14 files changed, 432 insertions(+), 203 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a5a379df6..ef997ccaca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,11 +54,15 @@ This page documents the version history and changes for the **mp-units** library - (!) feat: IAU system definition improved - feat: `uncertain` 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`), with measured constants diff --git a/docs/reference/systems_reference/.cache.json b/docs/reference/systems_reference/.cache.json index 86d9e40d4f..7f3c472060 100644 --- a/docs/reference/systems_reference/.cache.json +++ b/docs/reference/systems_reference/.cache.json @@ -1,3 +1,3 @@ { - "source_hash": "bf3cee58e59d6f79d66093d092d27b6ae26df036637c83328fac726e8668ffa3" + "source_hash": "c026b6a4ed2ccd02ec7e6b60a30f8c57595a9c2a1205758e5c0aa44824dce76d" } diff --git a/docs/reference/systems_reference/systems/codata.md b/docs/reference/systems_reference/systems/codata.md index bf251c39d5..9078b4979d 100644 --- a/docs/reference/systems_reference/systems/codata.md +++ b/docs/reference/systems_reference/systems/codata.md @@ -11,15 +11,15 @@ ## Constants -| Name | Symbol | unit_symbol | Definition | Relative standard uncertainty | -|------|:------:|:------------:|------------|:-----------------------------:| +| Name | Symbol | unit_symbol | Definition | Standard uncertainty | +|------|:------:|:------------:|------------|:--------------------:| | codata2014::magnetic_constant | μ₀ (u_0) | — | mag<4> * mag_power<10, -7> * [π](core.md#π) * [si::henry](si.md#henry) / [si::metre](si.md#metre) | — | -| codata2018::magnetic_constant | μ₀ (u_0) | — | mag_ratio<125'663'706'212, 100'000'000'000> * mag_power<10, -6> * [si::henry](si.md#henry) / [si::metre](si.md#metre) | mag_ratio<15, 10> * mag_power<10, -10> | -| codata2018::newtonian_constant_of_gravitation | G | — | mag_ratio<667'430, 100'000> * mag_power<10, -11> * cubic([si::metre](si.md#metre)) / [si::kilogram](si.md#kilogram) / square([si::second](si.md#second)) | mag_ratio<22, 10> * mag_power<10, -5> | -| codata2022::magnetic_constant | μ₀ (u_0) | — | mag_ratio<125'663'706'127, 100'000'000'000> * mag_power<10, -6> * [si::henry](si.md#henry) / [si::metre](si.md#metre) | mag_ratio<16, 10> * mag_power<10, -10> | -| codata2022::newtonian_constant_of_gravitation | G | — | mag_ratio<667'430, 100'000> * mag_power<10, -11> * cubic([si::metre](si.md#metre)) / [si::kilogram](si.md#kilogram) / square([si::second](si.md#second)) | mag_ratio<22, 10> * mag_power<10, -5> | +| codata2018::magnetic_constant | μ₀ (u_0) | — | mag_ratio<125'663'706'212, 100'000'000'000> * mag_power<10, -6> * [si::henry](si.md#henry) / [si::metre](si.md#metre) | mag_ratio<19, 10> * mag_power<10, -16> * [si::henry](si.md#henry) / [si::metre](si.md#metre) | +| codata2018::newtonian_constant_of_gravitation | G | — | mag_ratio<667'430, 100'000> * mag_power<10, -11> * cubic([si::metre](si.md#metre)) / [si::kilogram](si.md#kilogram) / square([si::second](si.md#second)) | mag_ratio<15, 10> * mag_power<10, -15> * cubic([si::metre](si.md#metre)) / [si::kilogram](si.md#kilogram) / square([si::second](si.md#second)) | +| codata2022::magnetic_constant | μ₀ (u_0) | — | mag_ratio<125'663'706'127, 100'000'000'000> * mag_power<10, -6> * [si::henry](si.md#henry) / [si::metre](si.md#metre) | mag_ratio<20, 10> * mag_power<10, -16> * [si::henry](si.md#henry) / [si::metre](si.md#metre) | +| codata2022::newtonian_constant_of_gravitation | G | — | mag_ratio<667'430, 100'000> * mag_power<10, -11> * cubic([si::metre](si.md#metre)) / [si::kilogram](si.md#kilogram) / square([si::second](si.md#second)) | mag_ratio<15, 10> * mag_power<10, -15> * cubic([si::metre](si.md#metre)) / [si::kilogram](si.md#kilogram) / square([si::second](si.md#second)) | -Constants with a relative standard uncertainty are measured rather than exact by definition, and satisfy the [`MeasuredConstant`](../../../users_guide/framework_basics/concepts.md#MeasuredConstant) concept. A `—` means no uncertainty is declared for that constant. +Constants with a standard uncertainty are measured rather than exact by definition, and satisfy the [`MeasuredConstant`](../../../users_guide/framework_basics/concepts.md#MeasuredConstant) concept. The uncertainty is stated in the form its source publishes: an absolute value in the constant's own dimension, or a relative one (marked *(relative)*) when only that form is published. A `—` means no uncertainty is declared for that constant. !!! note "Inline Namespaces" diff --git a/docs/reference/systems_reference/systems/hep.md b/docs/reference/systems_reference/systems/hep.md index bd35ccd0e1..bc1568d1d8 100644 --- a/docs/reference/systems_reference/systems/hep.md +++ b/docs/reference/systems_reference/systems/hep.md @@ -165,45 +165,45 @@ ## Constants -| Name | Symbol | unit_symbol | Definition | Relative standard uncertainty | -|------|:------:|:------------:|------------|:-----------------------------:| +| Name | Symbol | unit_symbol | Definition | Standard uncertainty | +|------|:------:|:------------:|------------|:--------------------:| | avogadro_constant | N_A | `N_A` | mag_ratio<602'214'076, 100'000'000> * mag_power<10, 23> / [mole](#mole) | — | -| codata2014::atomic_mass_unit | u | — | mag_ratio<9'314'940'954, 10'000'000> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | mag_ratio<62, 10> * mag_power<10, -9> | -| codata2014::bohr_magneton | μ_B (mu_B) | — | mag_ratio<9'274'009'994, 1'000'000'000> * mag_power<10, -24> * [joule](#joule) / [tesla](#tesla) | mag_ratio<62, 10> * mag_power<10, -9> | -| codata2014::bohr_radius | a_0 | — | mag_ratio<52'917'721'067, 10'000'000'000> * mag_power<10, -11> * [meter](#meter) | mag_ratio<23, 10> * mag_power<10, -10> | -| codata2014::boltzmann_constant | k_B | — | mag_ratio<86'173'303, 10'000'000> * mag_power<10, -11> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / [kelvin](#kelvin) | mag_ratio<57, 10> * mag_power<10, -7> | -| codata2014::classical_electron_radius | r_e | — | mag_ratio<28'179'403'227, 10'000'000'000> * mag_power<10, -15> * [meter](#meter) | mag_ratio<68, 10> * mag_power<10, -10> | -| codata2014::electron_compton_wavelength | λ_C (lambda_C) | — | mag_ratio<24'263'102'367, 10'000'000'000> * mag_power<10, -12> * [meter](#meter) | mag_ratio<45, 10> * mag_power<10, -10> | -| codata2014::electron_mass | m_e | — | mag_ratio<5'109'989'461, 10'000'000'000> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | mag_ratio<62, 10> * mag_power<10, -9> | -| codata2014::fine_structure_constant | α (alpha) | — | mag_ratio<72'973'525'664, 10'000'000'000> * mag_power<10, -3> * [one](core.md#one) | mag_ratio<23, 10> * mag_power<10, -10> | -| codata2014::neutron_mass | m_n | — | mag_ratio<9'395'654'133, 10'000'000> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | mag_ratio<62, 10> * mag_power<10, -9> | -| codata2014::nuclear_magneton | μ_N (mu_N) | — | mag_ratio<5'050'783'699, 1'000'000'000> * mag_power<10, -27> * [joule](#joule) / [tesla](#tesla) | mag_ratio<62, 10> * mag_power<10, -9> | +| codata2014::atomic_mass_unit | u | — | mag_ratio<9'314'940'954, 10'000'000> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | mag_ratio<57, 10> * mag_power<10, -6> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | +| codata2014::bohr_magneton | μ_B (mu_B) | — | mag_ratio<9'274'009'994, 1'000'000'000> * mag_power<10, -24> * [joule](#joule) / [tesla](#tesla) | mag_ratio<57, 10> * mag_power<10, -32> * [joule](#joule) / [tesla](#tesla) | +| codata2014::bohr_radius | a_0 | — | mag_ratio<52'917'721'067, 10'000'000'000> * mag_power<10, -11> * [meter](#meter) | mag_ratio<12, 10> * mag_power<10, -21> * [meter](#meter) | +| codata2014::boltzmann_constant | k_B | — | mag_ratio<86'173'303, 10'000'000> * mag_power<10, -11> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / [kelvin](#kelvin) | mag_ratio<50, 10> * mag_power<10, -17> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / [kelvin](#kelvin) | +| codata2014::classical_electron_radius | r_e | — | mag_ratio<28'179'403'227, 10'000'000'000> * mag_power<10, -15> * [meter](#meter) | mag_ratio<19, 10> * mag_power<10, -24> * [meter](#meter) | +| codata2014::electron_compton_wavelength | λ_C (lambda_C) | — | mag_ratio<24'263'102'367, 10'000'000'000> * mag_power<10, -12> * [meter](#meter) | mag_ratio<11, 10> * mag_power<10, -21> * [meter](#meter) | +| codata2014::electron_mass | m_e | — | mag_ratio<5'109'989'461, 10'000'000'000> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | mag_ratio<31, 10> * mag_power<10, -9> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | +| codata2014::fine_structure_constant | α (alpha) | — | mag_ratio<72'973'525'664, 10'000'000'000> * mag_power<10, -3> * [one](core.md#one) | mag_ratio<17, 10> * mag_power<10, -12> * [one](core.md#one) | +| codata2014::neutron_mass | m_n | — | mag_ratio<9'395'654'133, 10'000'000> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | mag_ratio<58, 10> * mag_power<10, -6> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | +| codata2014::nuclear_magneton | μ_N (mu_N) | — | mag_ratio<5'050'783'699, 1'000'000'000> * mag_power<10, -27> * [joule](#joule) / [tesla](#tesla) | mag_ratio<31, 10> * mag_power<10, -35> * [joule](#joule) / [tesla](#tesla) | | codata2014::permeability_of_vacuum | μ₀ (mu_0) | — | mag<4> * mag_power<10, -7> * [π](core.md#π) * [henry](#henry) / [meter](#meter) | — | -| codata2014::proton_mass | m_p | — | mag_ratio<9'382'720'813, 10'000'000> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | mag_ratio<62, 10> * mag_power<10, -9> | -| codata2018::atomic_mass_unit | u | `u` | mag_ratio<93'149'410'242, 100'000'000> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | mag_ratio<30, 10> * mag_power<10, -10> | -| codata2018::bohr_magneton | μ_B (mu_B) | `mu_B` | mag_ratio<92'740'100'783, 10'000'000'000> * mag_power<10, -24> * [joule](#joule) / [tesla](#tesla) | mag_ratio<30, 10> * mag_power<10, -10> | -| codata2018::bohr_radius | a_0 | `a_0` | mag_ratio<529'177'210'903, 100'000'000'000> * mag_power<10, -11> * [meter](#meter) | mag_ratio<15, 10> * mag_power<10, -10> | +| codata2014::proton_mass | m_p | — | mag_ratio<9'382'720'813, 10'000'000> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | mag_ratio<58, 10> * mag_power<10, -6> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | +| codata2018::atomic_mass_unit | u | `u` | mag_ratio<93'149'410'242, 100'000'000> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | mag_ratio<28, 10> * mag_power<10, -7> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | +| codata2018::bohr_magneton | μ_B (mu_B) | `mu_B` | mag_ratio<92'740'100'783, 10'000'000'000> * mag_power<10, -24> * [joule](#joule) / [tesla](#tesla) | mag_ratio<28, 10> * mag_power<10, -33> * [joule](#joule) / [tesla](#tesla) | +| codata2018::bohr_radius | a_0 | `a_0` | mag_ratio<529'177'210'903, 100'000'000'000> * mag_power<10, -11> * [meter](#meter) | mag_ratio<80, 10> * mag_power<10, -21> * [meter](#meter) | | codata2018::boltzmann_constant | k_B | `k_B` | mag_ratio<8'617'333'262, 1'000'000'000> * mag_power<10, -11> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / [kelvin](#kelvin) | — | -| codata2018::classical_electron_radius | r_e | `r_e` | mag_ratio<28'179'403'262, 10'000'000'000> * mag_power<10, -15> * [meter](#meter) | mag_ratio<45, 10> * mag_power<10, -10> | -| codata2018::electron_compton_wavelength | λ_C (lambda_C) | `lambda_C` | mag_ratio<242'631'023'867, 100'000'000'000> * mag_power<10, -12> * [meter](#meter) | mag_ratio<30, 10> * mag_power<10, -10> | -| codata2018::electron_mass | m_e | `m_e` | mag_ratio<51'099'895, 100'000'000> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | mag_ratio<30, 10> * mag_power<10, -10> | -| codata2018::fine_structure_constant | α (alpha) | `alpha` | mag_ratio<72'973'525'693, 10'000'000'000> * mag_power<10, -3> * [one](core.md#one) | mag_ratio<15, 10> * mag_power<10, -10> | -| codata2018::neutron_mass | m_n | `m_n` | mag_ratio<93'956'542'052, 100'000'000> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | mag_ratio<57, 10> * mag_power<10, -10> | -| codata2018::nuclear_magneton | μ_N (mu_N) | `mu_N` | mag_ratio<50'507'837'461, 10'000'000'000> * mag_power<10, -27> * [joule](#joule) / [tesla](#tesla) | mag_ratio<31, 10> * mag_power<10, -10> | -| codata2018::permeability_of_vacuum | μ₀ (mu_0) | `mu_0` | mag_ratio<125'663'706'212, 100'000'000'000> * mag_power<10, -6> * [henry](#henry) / [meter](#meter) | mag_ratio<15, 10> * mag_power<10, -10> | -| codata2018::proton_mass | m_p | `m_p` | mag_ratio<938'272'08816, 100'000'000> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | mag_ratio<31, 10> * mag_power<10, -10> | -| codata2022::atomic_mass_unit | u | — | mag_ratio<93'149'410'372, 100'000'000> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | mag_ratio<31, 10> * mag_power<10, -10> | -| codata2022::bohr_magneton | μ_B (mu_B) | — | mag_ratio<92'740'100'657, 10'000'000'000> * mag_power<10, -24> * [joule](#joule) / [tesla](#tesla) | mag_ratio<31, 10> * mag_power<10, -10> | -| codata2022::bohr_radius | a_0 | — | mag_ratio<529'177'210'544, 100'000'000'000> * mag_power<10, -11> * [meter](#meter) | mag_ratio<16, 10> * mag_power<10, -10> | +| codata2018::classical_electron_radius | r_e | `r_e` | mag_ratio<28'179'403'262, 10'000'000'000> * mag_power<10, -15> * [meter](#meter) | mag_ratio<13, 10> * mag_power<10, -24> * [meter](#meter) | +| codata2018::electron_compton_wavelength | λ_C (lambda_C) | `lambda_C` | mag_ratio<242'631'023'867, 100'000'000'000> * mag_power<10, -12> * [meter](#meter) | mag_ratio<73, 10> * mag_power<10, -22> * [meter](#meter) | +| codata2018::electron_mass | m_e | `m_e` | mag_ratio<51'099'895, 100'000'000> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | mag_ratio<15, 10> * mag_power<10, -10> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | +| codata2018::fine_structure_constant | α (alpha) | `alpha` | mag_ratio<72'973'525'693, 10'000'000'000> * mag_power<10, -3> * [one](core.md#one) | mag_ratio<11, 10> * mag_power<10, -12> * [one](core.md#one) | +| codata2018::neutron_mass | m_n | `m_n` | mag_ratio<93'956'542'052, 100'000'000> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | mag_ratio<54, 10> * mag_power<10, -7> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | +| codata2018::nuclear_magneton | μ_N (mu_N) | `mu_N` | mag_ratio<50'507'837'461, 10'000'000'000> * mag_power<10, -27> * [joule](#joule) / [tesla](#tesla) | mag_ratio<15, 10> * mag_power<10, -36> * [joule](#joule) / [tesla](#tesla) | +| codata2018::permeability_of_vacuum | μ₀ (mu_0) | `mu_0` | mag_ratio<125'663'706'212, 100'000'000'000> * mag_power<10, -6> * [henry](#henry) / [meter](#meter) | mag_ratio<19, 10> * mag_power<10, -16> * [henry](#henry) / [meter](#meter) | +| codata2018::proton_mass | m_p | `m_p` | mag_ratio<938'272'08816, 100'000'000> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | mag_ratio<29, 10> * mag_power<10, -7> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | +| codata2022::atomic_mass_unit | u | — | mag_ratio<93'149'410'372, 100'000'000> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | mag_ratio<29, 10> * mag_power<10, -7> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | +| codata2022::bohr_magneton | μ_B (mu_B) | — | mag_ratio<92'740'100'657, 10'000'000'000> * mag_power<10, -24> * [joule](#joule) / [tesla](#tesla) | mag_ratio<29, 10> * mag_power<10, -33> * [joule](#joule) / [tesla](#tesla) | +| codata2022::bohr_radius | a_0 | — | mag_ratio<529'177'210'544, 100'000'000'000> * mag_power<10, -11> * [meter](#meter) | mag_ratio<82, 10> * mag_power<10, -21> * [meter](#meter) | | codata2022::boltzmann_constant | k_B | — | alias to [codata2018::boltzmann_constant](#codata2018-boltzmann_constant) | — | -| codata2022::classical_electron_radius | r_e | — | mag_ratio<28'179'403'205, 10'000'000'000> * mag_power<10, -15> * [meter](#meter) | mag_ratio<47, 10> * mag_power<10, -10> | -| codata2022::electron_compton_wavelength | λ_C (lambda_C) | — | alias to [codata2018::electron_compton_wavelength](#codata2018-electron_compton_wavelength) | mag_ratio<30, 10> * mag_power<10, -10> | -| codata2022::electron_mass | m_e | — | mag_ratio<51'099'895'069, 100'000'000'000> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | mag_ratio<31, 10> * mag_power<10, -10> | -| codata2022::fine_structure_constant | α (alpha) | — | mag_ratio<72'973'525'643, 10'000'000'000> * mag_power<10, -3> * [one](core.md#one) | mag_ratio<16, 10> * mag_power<10, -10> | -| codata2022::neutron_mass | m_n | — | mag_ratio<93'956'542'194, 100'000'000> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | mag_ratio<51, 10> * mag_power<10, -10> | -| codata2022::nuclear_magneton | μ_N (mu_N) | — | mag_ratio<50'507'837'393, 10'000'000'000> * mag_power<10, -27> * [joule](#joule) / [tesla](#tesla) | mag_ratio<31, 10> * mag_power<10, -10> | -| codata2022::permeability_of_vacuum | μ₀ (mu_0) | — | mag_ratio<125'663'706'127, 100'000'000'000> * mag_power<10, -6> * [henry](#henry) / [meter](#meter) | mag_ratio<16, 10> * mag_power<10, -10> | -| codata2022::proton_mass | m_p | — | mag_ratio<93'827'208'943, 100'000'000> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | mag_ratio<31, 10> * mag_power<10, -10> | +| codata2022::classical_electron_radius | r_e | — | mag_ratio<28'179'403'205, 10'000'000'000> * mag_power<10, -15> * [meter](#meter) | mag_ratio<13, 10> * mag_power<10, -24> * [meter](#meter) | +| codata2022::electron_compton_wavelength | λ_C (lambda_C) | — | mag_ratio<242'631'023'538, 100'000'000'000> * mag_power<10, -12> * [meter](#meter) | mag_ratio<76, 10> * mag_power<10, -22> * [meter](#meter) | +| codata2022::electron_mass | m_e | — | mag_ratio<51'099'895'069, 100'000'000'000> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | mag_ratio<16, 10> * mag_power<10, -10> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | +| codata2022::fine_structure_constant | α (alpha) | — | mag_ratio<72'973'525'643, 10'000'000'000> * mag_power<10, -3> * [one](core.md#one) | mag_ratio<11, 10> * mag_power<10, -12> * [one](core.md#one) | +| codata2022::neutron_mass | m_n | — | mag_ratio<93'956'542'194, 100'000'000> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | mag_ratio<48, 10> * mag_power<10, -7> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | +| codata2022::nuclear_magneton | μ_N (mu_N) | — | mag_ratio<50'507'837'393, 10'000'000'000> * mag_power<10, -27> * [joule](#joule) / [tesla](#tesla) | mag_ratio<16, 10> * mag_power<10, -36> * [joule](#joule) / [tesla](#tesla) | +| codata2022::permeability_of_vacuum | μ₀ (mu_0) | — | mag_ratio<125'663'706'127, 100'000'000'000> * mag_power<10, -6> * [henry](#henry) / [meter](#meter) | mag_ratio<20, 10> * mag_power<10, -16> * [henry](#henry) / [meter](#meter) | +| codata2022::proton_mass | m_p | — | mag_ratio<93'827'208'943, 100'000'000> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | mag_ratio<29, 10> * mag_power<10, -7> * [si::mega](si.md#mega)<[electronvolt](#electronvolt)> / square([speed_of_light_in_vacuum](#speed_of_light_in_vacuum)) | | elementary_charge | e | `e` | [eplus](#eplus) | — | | gas_threshold | gas_threshold | — | mag<10> * [si::milli](si.md#milli)<[gram](#gram)> / cubic([si::centi](si.md#centi)<[meter](#meter)>) | — | | pi | π (pi) | — | alias to [mp_units::pi](#pi) | — | @@ -213,7 +213,7 @@ | standard_temperature | T_STP | — | mag_ratio<27'315, 100> * [kelvin](#kelvin) | — | | universe_mean_density | ρ_universe (rho_universe) | — | mag_power<10, -25> * [gram](#gram) / cubic([si::centi](si.md#centi)<[meter](#meter)>) | — | -Constants with a relative standard uncertainty are measured rather than exact by definition, and satisfy the [`MeasuredConstant`](../../../users_guide/framework_basics/concepts.md#MeasuredConstant) concept. A `—` means no uncertainty is declared for that constant. +Constants with a standard uncertainty are measured rather than exact by definition, and satisfy the [`MeasuredConstant`](../../../users_guide/framework_basics/concepts.md#MeasuredConstant) concept. The uncertainty is stated in the form its source publishes: an absolute value in the constant's own dimension, or a relative one (marked *(relative)*) when only that form is published. A `—` means no uncertainty is declared for that constant. !!! note "Inline Namespaces" diff --git a/docs/reference/systems_reference/systems/iau.md b/docs/reference/systems_reference/systems/iau.md index 2edb79e1c4..7d77590367 100644 --- a/docs/reference/systems_reference/systems/iau.md +++ b/docs/reference/systems_reference/systems/iau.md @@ -21,9 +21,9 @@ ## Constants -| Name | Symbol | unit_symbol | Definition | Relative standard uncertainty | -|------|:------:|:------------:|------------|:-----------------------------:| -| newtonian_constant_of_gravitation | G | `G` | alias to [codata::newtonian_constant_of_gravitation](codata.md#codata2022-newtonian_constant_of_gravitation) | mag_ratio<22, 10> * mag_power<10, -5> | +| Name | Symbol | unit_symbol | Definition | Standard uncertainty | +|------|:------:|:------------:|------------|:--------------------:| +| newtonian_constant_of_gravitation | G | `G` | alias to [codata::newtonian_constant_of_gravitation](codata.md#codata2022-newtonian_constant_of_gravitation) | mag_ratio<15, 10> * mag_power<10, -15> * cubic([si::metre](si.md#metre)) / [si::kilogram](si.md#kilogram) / square([si::second](si.md#second)) | | nominal_jovian_equatorial_radius | R_♃ₑᴺ (R_JUP_E_N) | `R_JUP_E_N` | mag_ratio<71'492, 10'000> * mag_power<10, 7> * [si::metre](si.md#metre) | — | | nominal_jovian_mass_parameter | (GM)_♃ᴺ ((GM)_JUP_N) | `GM_JUP_N` | mag_ratio<12'668'653, 10'000'000> * mag_power<10, 17> * cubic([si::metre](si.md#metre)) / square([si::second](si.md#second)) | — | | nominal_jovian_polar_radius | R_♃ₚᴺ (R_JUP_P_N) | `R_JUP_P_N` | mag_ratio<66'854, 10'000> * mag_power<10, 7> * [si::metre](si.md#metre) | — | @@ -36,4 +36,4 @@ | nominal_terrestrial_polar_radius | R_⊕ₚᴺ (R_EARTH_P_N) | `R_EARTH_P_N` | mag_ratio<63'568, 10'000> * mag_power<10, 6> * [si::metre](si.md#metre) | — | | nominal_total_solar_irradiance | S_☉ᴺ (S_SUN_N) | `S_SUN_N` | mag<1361> * [si::watt](si.md#watt) / square([si::metre](si.md#metre)) | — | -Constants with a relative standard uncertainty are measured rather than exact by definition, and satisfy the [`MeasuredConstant`](../../../users_guide/framework_basics/concepts.md#MeasuredConstant) concept. A `—` means no uncertainty is declared for that constant. +Constants with a standard uncertainty are measured rather than exact by definition, and satisfy the [`MeasuredConstant`](../../../users_guide/framework_basics/concepts.md#MeasuredConstant) concept. The uncertainty is stated in the form its source publishes: an absolute value in the constant's own dimension, or a relative one (marked *(relative)*) when only that form is published. A `—` means no uncertainty is declared for that constant. diff --git a/scripts/systems_reference.py b/scripts/systems_reference.py index faa8162750..f61da6ffe2 100644 --- a/scripts/systems_reference.py +++ b/scripts/systems_reference.py @@ -151,6 +151,10 @@ class Constant: # Relative standard uncertainty magnitude for a measured constant (e.g., # "mag_ratio<22, 10> * mag_power<10, -5>"); empty for constants that are exact by definition relative_standard_uncertainty: str = "" + # Absolute standard uncertainty expression for a measured constant declared with the + # `standard_uncertainty` wrapper (e.g., "mag_ratio<15, 10> * mag_power<10, -15> * m3 / kg / + # s2"); empty when the constant is exact or declares the relative form instead + standard_uncertainty: str = "" namespace: str = "" file: str = "" subnamespace: Optional[str] = None # Relative subnamespace (e.g., "codata2022") @@ -1070,6 +1074,7 @@ def _parse_units( symbol=const.symbol, definition=const.definition, relative_standard_uncertainty=const.relative_standard_uncertainty, + standard_uncertainty=const.standard_uncertainty, namespace=entity_full_namespace, file=file, alias_target=f"{full_namespace}::{unit_name}", @@ -1118,25 +1123,39 @@ def _extract_template_arg(self, text: str) -> str: return "".join(result).strip() _RSU_MARKER = ", relative_standard_uncertainty{" + _SU_MARKER = ", standard_uncertainty{" + + def _metadata_marker_index(self, definition: str) -> int: + """Index of the first uncertainty-wrapper argument, or -1 when there is none.""" + indices = [ + idx + for idx in ( + definition.find(self._RSU_MARKER), + definition.find(self._SU_MARKER), + ) + if idx != -1 + ] + return min(indices) if indices else -1 def _strip_constant_metadata(self, definition: str) -> str: """Drop trailing `named_constant` metadata arguments from a definition. - Metadata such as `relative_standard_uncertainty{...}` describes the constant, not how - its value is defined, so it gets its own column rather than sharing "Definition". + Metadata such as `relative_standard_uncertainty{...}` or `standard_uncertainty{...}` + describes the constant, not how its value is defined, so it gets its own column rather + than sharing "Definition". """ - idx = definition.find(self._RSU_MARKER) + idx = self._metadata_marker_index(definition) return definition[:idx].strip() if idx != -1 else definition - def _extract_relative_standard_uncertainty(self, definition: str) -> str: - """Extract the relative standard uncertainty magnitude from a raw constant definition. + def _extract_metadata_argument(self, definition: str, marker: str) -> str: + """Extract the content of an uncertainty-wrapper argument from a raw definition. - Returns an empty string for a constant that is exact by definition (no such argument). + Returns an empty string when the definition does not carry that wrapper. """ - idx = definition.find(self._RSU_MARKER) + idx = definition.find(marker) if idx == -1: return "" - inner = definition[idx + len(self._RSU_MARKER) :] + inner = definition[idx + len(marker) :] depth = 0 for pos, char in enumerate(inner): if char == "{": @@ -1147,6 +1166,20 @@ def _extract_relative_standard_uncertainty(self, definition: str) -> str: depth -= 1 return "" + def _extract_relative_standard_uncertainty(self, definition: str) -> str: + """Extract the relative standard uncertainty magnitude from a raw constant definition. + + Returns an empty string for a constant that is exact by definition (no such argument). + """ + return self._extract_metadata_argument(definition, self._RSU_MARKER) + + def _extract_standard_uncertainty(self, definition: str) -> str: + """Extract the absolute standard uncertainty expression from a raw constant definition. + + Returns an empty string for a constant that is exact by definition (no such argument). + """ + return self._extract_metadata_argument(definition, self._SU_MARKER) + def _parse_point_origins(self, content: str, system: SystemInfo, file: str): """Parse point origin definitions""" # Pattern for absolute and relative point origins: @@ -1309,6 +1342,7 @@ def _parse_constants(self, content: str, system: SystemInfo, file: str): definition_full = self._extract_template_arg(definition_raw) definition = self._strip_constant_metadata(definition_full) rsu = self._extract_relative_standard_uncertainty(definition_full) + su = self._extract_standard_uncertainty(definition_full) # Detect nested namespace (including inline namespaces for proper documentation) match_pos = match.start() @@ -1329,6 +1363,7 @@ def _parse_constants(self, content: str, system: SystemInfo, file: str): symbol=symbol, definition=definition, relative_standard_uncertainty=rsu, + standard_uncertainty=su, namespace=full_namespace, file=file, subnamespace=nested_ns, @@ -1344,6 +1379,7 @@ def _parse_constants(self, content: str, system: SystemInfo, file: str): definition_full = self._extract_template_arg(definition_raw) definition = self._strip_constant_metadata(definition_full) rsu = self._extract_relative_standard_uncertainty(definition_full) + su = self._extract_standard_uncertainty(definition_full) # Detect nested namespace (including inline namespaces for proper documentation) match_pos = match.start() @@ -1364,6 +1400,7 @@ def _parse_constants(self, content: str, system: SystemInfo, file: str): symbol=symbol, definition=definition, relative_standard_uncertainty=rsu, + standard_uncertainty=su, namespace=full_namespace, file=file, subnamespace=nested_ns, @@ -1788,6 +1825,7 @@ def _parse_using_declarations(self, content: str, system: SystemInfo, file: str) symbol=imported_constant.symbol, definition=f"using {full_namespace}::{unit_name}", relative_standard_uncertainty=imported_constant.relative_standard_uncertainty, + standard_uncertainty=imported_constant.standard_uncertainty, namespace=f"mp_units::{system.namespace}", file=file, alias_target=f"{full_namespace}::{unit_name}", @@ -3153,16 +3191,17 @@ def get_unit_display_name(unit): # narrower table. has_uncertainty = any( constant.relative_standard_uncertainty + or constant.standard_uncertainty for constant in system.constants ) if has_uncertainty: f.write( "| Name | Symbol | unit_symbol | Definition |" - " Relative standard uncertainty |\n" + " Standard uncertainty |\n" ) f.write( "|------|:------:|:------------:|------------|" - ":-----------------------------:|\n" + ":--------------------:|\n" ) else: f.write("| Name | Symbol | unit_symbol | Definition |\n") @@ -3211,18 +3250,9 @@ def get_constant_display_name(constant): else: symbols_display = "—" if has_uncertainty: - if constant.relative_standard_uncertainty: - rsu_display = "{}".format( - self._linkify_definition( - constant.relative_standard_uncertainty.replace( - "|", "\\|" - ), - system, - ) - ) - else: - rsu_display = "—" - rsu_cell = f" {rsu_display} |" + rsu_cell = ( + f" {self._uncertainty_cell(constant, system)} |" + ) else: rsu_cell = "" f.write( @@ -3248,21 +3278,12 @@ def get_constant_display_name(constant): definition, system ) if has_uncertainty: - if constant.relative_standard_uncertainty: - rsu_display = "{}".format( - self._linkify_definition( - constant.relative_standard_uncertainty.replace( - "|", "\\|" - ), - system, - ) - ) - else: - # Deliberately not "exact": the absence of the annotation - # only means it is not declared, and the audit of measured - # constants is not complete across all systems. - rsu_display = "—" - rsu_cell = f" {rsu_display} |" + # A "—" is deliberately not "exact": the absence of the + # annotation only means it is not declared, and the audit of + # measured constants is not complete across all systems. + rsu_cell = ( + f" {self._uncertainty_cell(constant, system)} |" + ) else: rsu_cell = "" f.write( @@ -3273,10 +3294,13 @@ def get_constant_display_name(constant): if has_uncertainty: f.write( - "\nConstants with a relative standard uncertainty are measured" - " rather than exact by definition, and satisfy the" + "\nConstants with a standard uncertainty are measured rather" + " than exact by definition, and satisfy the" " [`MeasuredConstant`](../../../users_guide/framework_basics/concepts.md#MeasuredConstant)" - " concept. A `—` means no uncertainty is declared for that" + " concept. The uncertainty is stated in the form its source" + " publishes: an absolute value in the constant's own dimension," + " or a relative one (marked *(relative)*) when only that form is" + " published. A `—` means no uncertainty is declared for that" " constant.\n" ) @@ -3562,6 +3586,27 @@ def _add_namespace_prefixes_to_definition(self, unit: Unit) -> str: return definition + def _uncertainty_cell(self, constant, system) -> str: + """Render the "Standard uncertainty" table cell for a constant. + + The declared form is shown as-is: the absolute one is a unit expression in the + constant's own dimension, while the relative one is a bare magnitude and gets marked + accordingly. A constant declares at most one of the two. + """ + if constant.standard_uncertainty: + return "{}".format( + self._linkify_definition( + constant.standard_uncertainty.replace("|", "\\|"), system + ) + ) + if constant.relative_standard_uncertainty: + return "{} *(relative)*".format( + self._linkify_definition( + constant.relative_standard_uncertainty.replace("|", "\\|"), system + ) + ) + return "—" + def _linkify_definition(self, definition: str, current_system: SystemInfo) -> str: """Convert unit/quantity/origin references in definition to markdown links while preserving code font. diff --git a/src/core/include/mp-units/bits/unit_conversion_impl.h b/src/core/include/mp-units/bits/unit_conversion_impl.h index 194bdef87b..452be5780b 100644 --- a/src/core/include/mp-units/bits/unit_conversion_impl.h +++ b/src/core/include/mp-units/bits/unit_conversion_impl.h @@ -260,9 +260,11 @@ consteval void collect_measured_constants(T, const named_constant) // the declared uncertainty covers the constant's whole published value, so nested measured - // constants (contributing only their central values to the magnitude) must not be added again - mp_units::detail::add_measured_constant_contribution(out, mp_units::detail::type_name(), num, den, - get_value(T::_relative_standard_uncertainty_)); + // constants (contributing only their central values to the magnitude) must not be added again; + // the accessor also derives `u_r` for constants that declare the absolute form + mp_units::detail::add_measured_constant_contribution( + out, mp_units::detail::type_name(), num, den, + get_value(mp_units::get_relative_standard_uncertainty(T{}))); else // an unannotated constant may still be defined in terms of measured ones mp_units::detail::collect_measured_constants(U, U, out, num, den); diff --git a/src/core/include/mp-units/framework/unit_concepts.h b/src/core/include/mp-units/framework/unit_concepts.h index 7371265449..b3105f8fa9 100644 --- a/src/core/include/mp-units/framework/unit_concepts.h +++ b/src/core/include/mp-units/framework/unit_concepts.h @@ -105,11 +105,13 @@ concept OffsetUnit = Unit && requires { T::_point_origin_; }; /** * @brief A concept matching constants that are measured rather than exact by definition * - * Satisfied by all constants defined with a `relative_standard_uncertainty` argument. Exact - * constants do not have that argument, so the distinction is visible to the type system. + * Satisfied by all constants defined with a `relative_standard_uncertainty` or a + * `standard_uncertainty` argument. Exact constants do not have such an argument, so the + * distinction is visible to the type system. */ MP_UNITS_EXPORT template concept MeasuredConstant = - Unit && requires { requires UnitMagnitude; }; + Unit && (requires { requires UnitMagnitude; } || + requires { requires Unit; }); } // namespace mp_units diff --git a/src/core/include/mp-units/framework/unit_definitions.h b/src/core/include/mp-units/framework/unit_definitions.h index a218916d8d..372e204411 100644 --- a/src/core/include/mp-units/framework/unit_definitions.h +++ b/src/core/include/mp-units/framework/unit_definitions.h @@ -447,6 +447,12 @@ struct named_unit : decltype(U)::_base_type_ { * metadata only - it never participates in unit equality, conversion factors, or symbolic * simplification. * + * A measured constant declares exactly one of `relative_standard_uncertainty` and + * `standard_uncertainty` - whichever form its source publishes. The published pair is mutually + * rounded, so carrying both would embed a contradiction. This form is invariant under a change + * of unit, so it is the right choice for a constant whose defining unit has no published + * absolute uncertainty. + * * For example: * * @code{.cpp} @@ -470,6 +476,44 @@ relative_standard_uncertainty(M) -> relative_standard_uncertainty; #endif +/** + * @brief Absolute standard uncertainty of a measured constant + * + * Wraps the standard uncertainty (GUM/CODATA `u`) of a measured constant as an exact symbolic + * expression of the same shape as the constant's value: a magnitude times a unit of the same + * dimension. Spelling the unit out makes the expression self-contained - the relative form + * derives as an exact ratio of two canonical magnitudes, with the units cancelling exactly. + * Like `relative_standard_uncertainty`, it is metadata only. + * + * A measured constant declares exactly one of the two wrappers - whichever form its source + * publishes. Metrology tables (e.g., CODATA) tabulate the absolute uncertainty next to the + * value, both rounded consistently, so a definition transcribing a table row should use this + * form; deriving it from a published `u_r` instead can be off by several percent in the last + * digit. + * + * For example: + * + * @code{.cpp} + * inline constexpr struct newtonian_constant_of_gravitation final : + * named_constant<"G", mag_ratio<667'430, 100'000> * mag_power<10, -11> * m3 / kg / s2, + * standard_uncertainty{mag_ratio<15, 10> * mag_power<10, -15> * m3 / kg / s2}> {} + * newtonian_constant_of_gravitation; + * @endcode + * + * @tparam U a unit expression representing the standard uncertainty in the constant's dimension + */ +MP_UNITS_EXPORT template +struct standard_uncertainty { + U unit; +}; + +#if MP_UNITS_COMP_CLANG && MP_UNITS_COMP_CLANG < 17 + +template +standard_uncertainty(U) -> standard_uncertainty; + +#endif + /** * @brief Named constant definition * @@ -489,7 +533,7 @@ struct named_constant : decltype(U)::_base_type_ { }; /** - * @brief Specialization for a measured constant + * @brief Specialization for a measured constant declaring its relative standard uncertainty * * The same as the above but additionally stores the relative standard uncertainty of a constant * that is measured rather than exact by definition. The value transcribes the entry published in @@ -510,17 +554,65 @@ struct named_constant : decltype(U)::_base_type_ { static constexpr auto _relative_standard_uncertainty_ = M{}; }; +/** + * @brief Specialization for a measured constant declaring its absolute standard uncertainty + * + * The same as the above but stores the standard uncertainty in the form the source metrology + * table publishes it: an absolute value in a unit of the constant's own dimension. The relative + * form derives on demand as an exact ratio of canonical magnitudes. + * + * @tparam Symbol a short text representation of the constant + * @tparam Unit a unit that we use to define a constant + * @tparam SU the standard uncertainty of the measured value + */ +template SU> + requires(!Symbol.empty()) && detail::is_positive_canonical_unit_mag && detail::ConvertibleUnits +struct named_constant : decltype(U)::_base_type_ { + using _base_type_ = named_constant; // exposition only + static constexpr auto _symbol_ = Symbol; ///< Unique constant identifier + static constexpr auto _standard_uncertainty_ = SUU{}; +}; + /** * @brief Returns the relative standard uncertainty of a measured constant * + * For a constant declared with `standard_uncertainty`, the result is derived as the exact ratio + * of the uncertainty's canonical magnitude to the constant's one (`u_r = u(x)/|x|`, so the + * result never carries a sign even for a negative-valued constant). + * * There is no valid result for constants that are exact by definition. Asking for one would * conflate "exact by definition" with "measured infinitely precisely", so it is a compile-time * error rather than a zero magnitude. */ MP_UNITS_EXPORT template -[[nodiscard]] consteval UnitMagnitude auto get_relative_standard_uncertainty(U) +[[nodiscard]] consteval UnitMagnitude auto get_relative_standard_uncertainty(U u) { - return U::_relative_standard_uncertainty_; + if constexpr (requires { requires UnitMagnitude; }) + return U::_relative_standard_uncertainty_; + else + return detail::abs_magnitude(get_canonical_unit(U::_standard_uncertainty_).mag / get_canonical_unit(u).mag); +} + +/** + * @brief Returns the standard uncertainty of a measured constant + * + * The result is a unit expression of the constant's own dimension (constants are units in this + * library, and so are their uncertainties). For a constant declared with + * `relative_standard_uncertainty`, the result is derived by scaling the constant itself + * (`u(x) = u_r * |x|`, so the result is positive even for a negative-valued constant). + * + * As for `get_relative_standard_uncertainty`, there is no valid result for constants that are + * exact by definition. + */ +MP_UNITS_EXPORT template +[[nodiscard]] consteval Unit auto get_standard_uncertainty(U u) +{ + if constexpr (requires { requires Unit; }) + return U::_standard_uncertainty_; + else if constexpr (detail::is_positive_canonical_unit_mag) + return U::_relative_standard_uncertainty_ * u; + else + return (-U::_relative_standard_uncertainty_) * u; } diff --git a/src/systems/include/mp-units/systems/codata.h b/src/systems/include/mp-units/systems/codata.h index 93b809634c..03d9dbda28 100644 --- a/src/systems/include/mp-units/systems/codata.h +++ b/src/systems/include/mp-units/systems/codata.h @@ -67,13 +67,13 @@ namespace codata2018 { // clang-format off // 1.256 637 062 12(19) × 10⁻⁶ N/A²; measured since the ampere was redefined via the elementary charge inline constexpr struct magnetic_constant final : - named_constant * mag_power<10, -6> * si::henry / si::metre, relative_standard_uncertainty{mag_ratio<15, 10> * mag_power<10, -10>}> {} magnetic_constant; + named_constant * mag_power<10, -6> * si::henry / si::metre, standard_uncertainty{mag_ratio<19, 10> * mag_power<10, -16> * si::henry / si::metre}> {} magnetic_constant; -// 6.674 30(15) × 10⁻¹¹ m³ kg⁻¹ s⁻². The relative form is stored rather than the absolute one shown -// in parentheses, because only the relative form is invariant under a change of unit. -// https://physics.nist.gov/cgi-bin/cuu/Value?bg +// 6.674 30(15) × 10⁻¹¹ m³ kg⁻¹ s⁻². Value and uncertainty are both taken verbatim from the table; +// the published pair is mutually rounded, so the absolute form reproduces the published σ exactly +// while a stored `u_r` would not. https://physics.nist.gov/cgi-bin/cuu/Value?bg 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>}> {} newtonian_constant_of_gravitation; + 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; // clang-format on } // namespace codata2018 @@ -83,12 +83,12 @@ inline namespace codata2022 { // clang-format off // 1.256 637 061 27(20) × 10⁻⁶ N/A² inline constexpr struct magnetic_constant final : - named_constant * mag_power<10, -6> * si::henry / si::metre, relative_standard_uncertainty{mag_ratio<16, 10> * mag_power<10, -10>}> {} magnetic_constant; + named_constant * mag_power<10, -6> * si::henry / si::metre, standard_uncertainty{mag_ratio<20, 10> * mag_power<10, -16> * si::henry / si::metre}> {} magnetic_constant; // Unchanged from the 2018 adjustment: 6.674 30(15) × 10⁻¹¹ m³ kg⁻¹ s⁻². // https://physics.nist.gov/cgi-bin/cuu/Value?bg 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>}> {} newtonian_constant_of_gravitation; + 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; // clang-format on } // namespace codata2022 diff --git a/src/systems/include/mp-units/systems/hep/constants.h b/src/systems/include/mp-units/systems/hep/constants.h index ac08cf8810..68c2cb37ab 100644 --- a/src/systems/include/mp-units/systems/hep/constants.h +++ b/src/systems/include/mp-units/systems/hep/constants.h @@ -107,47 +107,50 @@ namespace codata2014 { inline constexpr struct permeability_of_vacuum final : named_constant * mag_power<10, -7> * π * henry / meter> {} permeability_of_vacuum; -// Boltzmann constant (CODATA 2014: 8.6173303e-11 MeV/K) +// Boltzmann constant (CODATA 2014: 8.617 3303(50) e-11 MeV/K, from the eV/K table entry) inline constexpr struct boltzmann_constant final : - named_constant<"k_B", mag_ratio<86'173'303, 10'000'000> * mag_power<10, -11> * si::mega / kelvin, relative_standard_uncertainty{mag_ratio<57, 10> * mag_power<10, -7>}> {} boltzmann_constant; + named_constant<"k_B", mag_ratio<86'173'303, 10'000'000> * mag_power<10, -11> * si::mega / kelvin, standard_uncertainty{mag_ratio<50, 10> * mag_power<10, -17> * si::mega / kelvin}> {} boltzmann_constant; // Particle masses (CODATA 2014) +// Electron mass (CODATA 2014: 0.510 998 9461(31) MeV/c²) inline constexpr struct electron_mass final : - named_constant<"m_e", mag_ratio<5'109'989'461, 10'000'000'000> * si::mega / square(speed_of_light_in_vacuum), relative_standard_uncertainty{mag_ratio<62, 10> * mag_power<10, -9>}> {} electron_mass; + named_constant<"m_e", mag_ratio<5'109'989'461, 10'000'000'000> * si::mega / square(speed_of_light_in_vacuum), standard_uncertainty{mag_ratio<31, 10> * mag_power<10, -9> * si::mega / square(speed_of_light_in_vacuum)}> {} electron_mass; +// Proton mass (CODATA 2014: 938.272 0813(58) MeV/c²) inline constexpr struct proton_mass final : - named_constant<"m_p", mag_ratio<9'382'720'813, 10'000'000> * si::mega / square(speed_of_light_in_vacuum), relative_standard_uncertainty{mag_ratio<62, 10> * mag_power<10, -9>}> {} proton_mass; + named_constant<"m_p", mag_ratio<9'382'720'813, 10'000'000> * si::mega / square(speed_of_light_in_vacuum), standard_uncertainty{mag_ratio<58, 10> * mag_power<10, -6> * si::mega / square(speed_of_light_in_vacuum)}> {} proton_mass; +// Neutron mass (CODATA 2014: 939.565 4133(58) MeV/c²) inline constexpr struct neutron_mass final : - named_constant<"m_n", mag_ratio<9'395'654'133, 10'000'000> * si::mega / square(speed_of_light_in_vacuum), relative_standard_uncertainty{mag_ratio<62, 10> * mag_power<10, -9>}> {} neutron_mass; + named_constant<"m_n", mag_ratio<9'395'654'133, 10'000'000> * si::mega / square(speed_of_light_in_vacuum), standard_uncertainty{mag_ratio<58, 10> * mag_power<10, -6> * si::mega / square(speed_of_light_in_vacuum)}> {} neutron_mass; -// Atomic mass unit (CODATA 2014) +// Atomic mass unit (CODATA 2014: 931.494 0954(57) MeV/c²) inline constexpr struct atomic_mass_unit final : - named_constant<"u", mag_ratio<9'314'940'954, 10'000'000> * si::mega / square(speed_of_light_in_vacuum), relative_standard_uncertainty{mag_ratio<62, 10> * mag_power<10, -9>}> {} atomic_mass_unit; + named_constant<"u", mag_ratio<9'314'940'954, 10'000'000> * si::mega / square(speed_of_light_in_vacuum), standard_uncertainty{mag_ratio<57, 10> * mag_power<10, -6> * si::mega / square(speed_of_light_in_vacuum)}> {} atomic_mass_unit; -// Fine structure constant (CODATA 2014: 7.2973525664e-3) +// Fine structure constant (CODATA 2014: 7.297 352 5664(17) e-3) inline constexpr struct fine_structure_constant final : - named_constant * mag_power<10, -3> * one, relative_standard_uncertainty{mag_ratio<23, 10> * mag_power<10, -10>}> {} fine_structure_constant; + named_constant * mag_power<10, -3> * one, standard_uncertainty{mag_ratio<17, 10> * mag_power<10, -12> * one}> {} fine_structure_constant; -// Classical electron radius (CODATA 2014: 2.8179403227e-15 m) +// Classical electron radius (CODATA 2014: 2.817 940 3227(19) e-15 m) inline constexpr struct classical_electron_radius final : - named_constant<"r_e", mag_ratio<28'179'403'227, 10'000'000'000> * mag_power<10, -15> * meter, relative_standard_uncertainty{mag_ratio<68, 10> * mag_power<10, -10>}> {} classical_electron_radius; + named_constant<"r_e", mag_ratio<28'179'403'227, 10'000'000'000> * mag_power<10, -15> * meter, standard_uncertainty{mag_ratio<19, 10> * mag_power<10, -24> * meter}> {} classical_electron_radius; -// Electron Compton wavelength (CODATA 2014: 2.4263102367e-12 m) +// Electron Compton wavelength (CODATA 2014: 2.426 310 2367(11) e-12 m) inline constexpr struct electron_compton_wavelength final : - named_constant * mag_power<10, -12> * meter, relative_standard_uncertainty{mag_ratio<45, 10> * mag_power<10, -10>}> {} electron_compton_wavelength; + named_constant * mag_power<10, -12> * meter, standard_uncertainty{mag_ratio<11, 10> * mag_power<10, -21> * meter}> {} electron_compton_wavelength; -// Bohr radius (CODATA 2014: 5.2917721067e-11 m) +// Bohr radius (CODATA 2014: 5.291 772 1067(12) e-11 m) inline constexpr struct bohr_radius final : - named_constant<"a_0", mag_ratio<52'917'721'067, 10'000'000'000> * mag_power<10, -11> * meter, relative_standard_uncertainty{mag_ratio<23, 10> * mag_power<10, -10>}> {} bohr_radius; + named_constant<"a_0", mag_ratio<52'917'721'067, 10'000'000'000> * mag_power<10, -11> * meter, standard_uncertainty{mag_ratio<12, 10> * mag_power<10, -21> * meter}> {} bohr_radius; -// Bohr magneton (CODATA 2014: 9.274009994e-24 J/T) +// Bohr magneton (CODATA 2014: 9.274 009 994(57) e-24 J/T) inline constexpr struct bohr_magneton final : - named_constant * mag_power<10, -24> * joule / tesla, relative_standard_uncertainty{mag_ratio<62, 10> * mag_power<10, -9>}> {} bohr_magneton; + named_constant * mag_power<10, -24> * joule / tesla, standard_uncertainty{mag_ratio<57, 10> * mag_power<10, -32> * joule / tesla}> {} bohr_magneton; -// Nuclear magneton (CODATA 2014: 5.050783699e-27 J/T) +// Nuclear magneton (CODATA 2014: 5.050 783 699(31) e-27 J/T) inline constexpr struct nuclear_magneton final : - named_constant * mag_power<10, -27> * joule / tesla, relative_standard_uncertainty{mag_ratio<62, 10> * mag_power<10, -9>}> {} nuclear_magneton; + named_constant * mag_power<10, -27> * joule / tesla, standard_uncertainty{mag_ratio<31, 10> * mag_power<10, -35> * joule / tesla}> {} nuclear_magneton; // clang-format on @@ -158,10 +161,10 @@ inline namespace codata2018 { // clang-format off -// Vacuum magnetic permeability (CODATA 2018: 1.256 637 062 12(19) × 10⁻⁶ N/A², u_r = 1.5 × 10⁻¹⁰) +// Vacuum magnetic permeability (CODATA 2018: 1.256 637 062 12(19) × 10⁻⁶ N/A²) // No longer exact: the 2019 SI redefinition fixed `e` instead, so μ₀ is now measured. inline constexpr struct permeability_of_vacuum final : - named_constant * mag_power<10, -6> * henry / meter, relative_standard_uncertainty{mag_ratio<15, 10> * mag_power<10, -10>}> {} permeability_of_vacuum; + named_constant * mag_power<10, -6> * henry / meter, standard_uncertainty{mag_ratio<19, 10> * mag_power<10, -16> * henry / meter}> {} permeability_of_vacuum; // Boltzmann constant (CODATA 2018: 8.617333262e-11 MeV/K) // No `relative_standard_uncertainty`: since the 2019 SI redefinition both k_B and the elementary @@ -172,42 +175,45 @@ inline constexpr struct boltzmann_constant final : named_constant<"k_B", mag_ratio<8'617'333'262, 1'000'000'000> * mag_power<10, -11> * si::mega / kelvin> {} boltzmann_constant; // Particle masses (CODATA 2018) +// Electron mass (CODATA 2018: 0.510 998 950 00(15) MeV/c²) inline constexpr struct electron_mass final : - named_constant<"m_e", mag_ratio<51'099'895, 100'000'000> * si::mega / square(speed_of_light_in_vacuum), relative_standard_uncertainty{mag_ratio<30, 10> * mag_power<10, -10>}> {} electron_mass; + named_constant<"m_e", mag_ratio<51'099'895, 100'000'000> * si::mega / square(speed_of_light_in_vacuum), standard_uncertainty{mag_ratio<15, 10> * mag_power<10, -10> * si::mega / square(speed_of_light_in_vacuum)}> {} electron_mass; +// Proton mass (CODATA 2018: 938.272 088 16(29) MeV/c²) inline constexpr struct proton_mass final : - named_constant<"m_p", mag_ratio<938'272'08816, 100'000'000> * si::mega / square(speed_of_light_in_vacuum), relative_standard_uncertainty{mag_ratio<31, 10> * mag_power<10, -10>}> {} proton_mass; + named_constant<"m_p", mag_ratio<938'272'08816, 100'000'000> * si::mega / square(speed_of_light_in_vacuum), standard_uncertainty{mag_ratio<29, 10> * mag_power<10, -7> * si::mega / square(speed_of_light_in_vacuum)}> {} proton_mass; +// Neutron mass (CODATA 2018: 939.565 420 52(54) MeV/c²) inline constexpr struct neutron_mass final : - named_constant<"m_n", mag_ratio<93'956'542'052, 100'000'000> * si::mega / square(speed_of_light_in_vacuum), relative_standard_uncertainty{mag_ratio<57, 10> * mag_power<10, -10>}> {} neutron_mass; + named_constant<"m_n", mag_ratio<93'956'542'052, 100'000'000> * si::mega / square(speed_of_light_in_vacuum), standard_uncertainty{mag_ratio<54, 10> * mag_power<10, -7> * si::mega / square(speed_of_light_in_vacuum)}> {} neutron_mass; -// Atomic mass unit (CODATA 2018) +// Atomic mass unit (CODATA 2018: 931.494 102 42(28) MeV/c²) inline constexpr struct atomic_mass_unit final : - named_constant<"u", mag_ratio<93'149'410'242, 100'000'000> * si::mega / square(speed_of_light_in_vacuum), relative_standard_uncertainty{mag_ratio<30, 10> * mag_power<10, -10>}> {} atomic_mass_unit; + named_constant<"u", mag_ratio<93'149'410'242, 100'000'000> * si::mega / square(speed_of_light_in_vacuum), standard_uncertainty{mag_ratio<28, 10> * mag_power<10, -7> * si::mega / square(speed_of_light_in_vacuum)}> {} atomic_mass_unit; -// Fine structure constant (CODATA 2018: 7.2973525693e-3) +// Fine structure constant (CODATA 2018: 7.297 352 5693(11) e-3) inline constexpr struct fine_structure_constant final : - named_constant * mag_power<10, -3> * one, relative_standard_uncertainty{mag_ratio<15, 10> * mag_power<10, -10>}> {} fine_structure_constant; + named_constant * mag_power<10, -3> * one, standard_uncertainty{mag_ratio<11, 10> * mag_power<10, -12> * one}> {} fine_structure_constant; -// Classical electron radius (CODATA 2018: 2.8179403262e-15 m) +// Classical electron radius (CODATA 2018: 2.817 940 3262(13) e-15 m) inline constexpr struct classical_electron_radius final : - named_constant<"r_e", mag_ratio<28'179'403'262, 10'000'000'000> * mag_power<10, -15> * meter, relative_standard_uncertainty{mag_ratio<45, 10> * mag_power<10, -10>}> {} classical_electron_radius; + named_constant<"r_e", mag_ratio<28'179'403'262, 10'000'000'000> * mag_power<10, -15> * meter, standard_uncertainty{mag_ratio<13, 10> * mag_power<10, -24> * meter}> {} classical_electron_radius; -// Electron Compton wavelength (CODATA 2018: 2.42631023867e-12 m) +// Electron Compton wavelength (CODATA 2018: 2.426 310 238 67(73) e-12 m) inline constexpr struct electron_compton_wavelength final : - named_constant * mag_power<10, -12> * meter, relative_standard_uncertainty{mag_ratio<30, 10> * mag_power<10, -10>}> {} electron_compton_wavelength; + named_constant * mag_power<10, -12> * meter, standard_uncertainty{mag_ratio<73, 10> * mag_power<10, -22> * meter}> {} electron_compton_wavelength; -// Bohr radius (CODATA 2018: 5.29177210903e-11 m) +// Bohr radius (CODATA 2018: 5.291 772 109 03(80) e-11 m) inline constexpr struct bohr_radius final : - named_constant<"a_0", mag_ratio<529'177'210'903, 100'000'000'000> * mag_power<10, -11> * meter, relative_standard_uncertainty{mag_ratio<15, 10> * mag_power<10, -10>}> {} bohr_radius; + named_constant<"a_0", mag_ratio<529'177'210'903, 100'000'000'000> * mag_power<10, -11> * meter, standard_uncertainty{mag_ratio<80, 10> * mag_power<10, -21> * meter}> {} bohr_radius; -// Bohr magneton (CODATA 2018: 9.2740100783e-24 J/T) +// Bohr magneton (CODATA 2018: 9.274 010 0783(28) e-24 J/T) inline constexpr struct bohr_magneton final : - named_constant * mag_power<10, -24> * joule / tesla, relative_standard_uncertainty{mag_ratio<30, 10> * mag_power<10, -10>}> {} bohr_magneton; + named_constant * mag_power<10, -24> * joule / tesla, standard_uncertainty{mag_ratio<28, 10> * mag_power<10, -33> * joule / tesla}> {} bohr_magneton; -// Nuclear magneton (CODATA 2018: 5.0507837461e-27 J/T) +// Nuclear magneton (CODATA 2018: 5.050 783 7461(15) e-27 J/T) inline constexpr struct nuclear_magneton final : - named_constant * mag_power<10, -27> * joule / tesla, relative_standard_uncertainty{mag_ratio<31, 10> * mag_power<10, -10>}> {} nuclear_magneton; + named_constant * mag_power<10, -27> * joule / tesla, standard_uncertainty{mag_ratio<15, 10> * mag_power<10, -36> * joule / tesla}> {} nuclear_magneton; // clang-format on @@ -221,46 +227,51 @@ namespace codata2022 { // Boltzmann constant (CODATA 2022: 8.617333262e-11 MeV/K, exact - same as 2018) using codata2018::boltzmann_constant; -// Vacuum magnetic permeability (CODATA 2022: 1.256 637 061 27(20) × 10⁻⁶ N/A², u_r = 1.6 × 10⁻¹⁰) +// Vacuum magnetic permeability (CODATA 2022: 1.256 637 061 27(20) × 10⁻⁶ N/A²) inline constexpr struct permeability_of_vacuum final : - named_constant * mag_power<10, -6> * henry / meter, relative_standard_uncertainty{mag_ratio<16, 10> * mag_power<10, -10>}> {} permeability_of_vacuum; + named_constant * mag_power<10, -6> * henry / meter, standard_uncertainty{mag_ratio<20, 10> * mag_power<10, -16> * henry / meter}> {} permeability_of_vacuum; // Particle masses (CODATA 2022) +// Electron mass (CODATA 2022: 0.510 998 950 69(16) MeV/c²) inline constexpr struct electron_mass final : - named_constant<"m_e", mag_ratio<51'099'895'069, 100'000'000'000> * si::mega / square(speed_of_light_in_vacuum), relative_standard_uncertainty{mag_ratio<31, 10> * mag_power<10, -10>}> {} electron_mass; + named_constant<"m_e", mag_ratio<51'099'895'069, 100'000'000'000> * si::mega / square(speed_of_light_in_vacuum), standard_uncertainty{mag_ratio<16, 10> * mag_power<10, -10> * si::mega / square(speed_of_light_in_vacuum)}> {} electron_mass; +// Proton mass (CODATA 2022: 938.272 089 43(29) MeV/c²) inline constexpr struct proton_mass final : - named_constant<"m_p", mag_ratio<93'827'208'943, 100'000'000> * si::mega / square(speed_of_light_in_vacuum), relative_standard_uncertainty{mag_ratio<31, 10> * mag_power<10, -10>}> {} proton_mass; + named_constant<"m_p", mag_ratio<93'827'208'943, 100'000'000> * si::mega / square(speed_of_light_in_vacuum), standard_uncertainty{mag_ratio<29, 10> * mag_power<10, -7> * si::mega / square(speed_of_light_in_vacuum)}> {} proton_mass; +// Neutron mass (CODATA 2022: 939.565 421 94(48) MeV/c²) inline constexpr struct neutron_mass final : - named_constant<"m_n", mag_ratio<93'956'542'194, 100'000'000> * si::mega / square(speed_of_light_in_vacuum), relative_standard_uncertainty{mag_ratio<51, 10> * mag_power<10, -10>}> {} neutron_mass; + named_constant<"m_n", mag_ratio<93'956'542'194, 100'000'000> * si::mega / square(speed_of_light_in_vacuum), standard_uncertainty{mag_ratio<48, 10> * mag_power<10, -7> * si::mega / square(speed_of_light_in_vacuum)}> {} neutron_mass; -// Atomic mass unit (CODATA 2022) +// Atomic mass unit (CODATA 2022: 931.494 103 72(29) MeV/c²) inline constexpr struct atomic_mass_unit final : - named_constant<"u", mag_ratio<93'149'410'372, 100'000'000> * si::mega / square(speed_of_light_in_vacuum), relative_standard_uncertainty{mag_ratio<31, 10> * mag_power<10, -10>}> {} atomic_mass_unit; + named_constant<"u", mag_ratio<93'149'410'372, 100'000'000> * si::mega / square(speed_of_light_in_vacuum), standard_uncertainty{mag_ratio<29, 10> * mag_power<10, -7> * si::mega / square(speed_of_light_in_vacuum)}> {} atomic_mass_unit; -// Fine structure constant (CODATA 2022: 7.2973525643e-3) +// Fine structure constant (CODATA 2022: 7.297 352 5643(11) e-3) inline constexpr struct fine_structure_constant final : - named_constant * mag_power<10, -3> * one, relative_standard_uncertainty{mag_ratio<16, 10> * mag_power<10, -10>}> {} fine_structure_constant; + named_constant * mag_power<10, -3> * one, standard_uncertainty{mag_ratio<11, 10> * mag_power<10, -12> * one}> {} fine_structure_constant; -// Classical electron radius (CODATA 2022: 2.8179403205e-15 m) +// Classical electron radius (CODATA 2022: 2.817 940 3205(13) e-15 m) inline constexpr struct classical_electron_radius final : - named_constant<"r_e", mag_ratio<28'179'403'205, 10'000'000'000> * mag_power<10, -15> * meter, relative_standard_uncertainty{mag_ratio<47, 10> * mag_power<10, -10>}> {} classical_electron_radius; + named_constant<"r_e", mag_ratio<28'179'403'205, 10'000'000'000> * mag_power<10, -15> * meter, standard_uncertainty{mag_ratio<13, 10> * mag_power<10, -24> * meter}> {} classical_electron_radius; -// Electron Compton wavelength (CODATA 2022: 2.42631023867e-12 m - same as 2018) -using codata2018::electron_compton_wavelength; +// Electron Compton wavelength (CODATA 2022: 2.426 310 235 38(76) e-12 m) +// The 2022 adjustment shifted this value, so it is NOT the 2018 one under a new name. +inline constexpr struct electron_compton_wavelength final : + named_constant * mag_power<10, -12> * meter, standard_uncertainty{mag_ratio<76, 10> * mag_power<10, -22> * meter}> {} electron_compton_wavelength; -// Bohr radius (CODATA 2022: 5.29177210544e-11 m) +// Bohr radius (CODATA 2022: 5.291 772 105 44(82) e-11 m) inline constexpr struct bohr_radius final : - named_constant<"a_0", mag_ratio<529'177'210'544, 100'000'000'000> * mag_power<10, -11> * meter, relative_standard_uncertainty{mag_ratio<16, 10> * mag_power<10, -10>}> {} bohr_radius; + named_constant<"a_0", mag_ratio<529'177'210'544, 100'000'000'000> * mag_power<10, -11> * meter, standard_uncertainty{mag_ratio<82, 10> * mag_power<10, -21> * meter}> {} bohr_radius; -// Bohr magneton (CODATA 2022: 9.2740100657e-24 J/T) +// Bohr magneton (CODATA 2022: 9.274 010 0657(29) e-24 J/T) inline constexpr struct bohr_magneton final : - named_constant * mag_power<10, -24> * joule / tesla, relative_standard_uncertainty{mag_ratio<31, 10> * mag_power<10, -10>}> {} bohr_magneton; + named_constant * mag_power<10, -24> * joule / tesla, standard_uncertainty{mag_ratio<29, 10> * mag_power<10, -33> * joule / tesla}> {} bohr_magneton; -// Nuclear magneton (CODATA 2022: 5.0507837393e-27 J/T) +// Nuclear magneton (CODATA 2022: 5.050 783 7393(16) e-27 J/T) inline constexpr struct nuclear_magneton final : - named_constant * mag_power<10, -27> * joule / tesla, relative_standard_uncertainty{mag_ratio<31, 10> * mag_power<10, -10>}> {} nuclear_magneton; + named_constant * mag_power<10, -27> * joule / tesla, standard_uncertainty{mag_ratio<16, 10> * mag_power<10, -36> * joule / tesla}> {} nuclear_magneton; // clang-format on diff --git a/src/utility/include/mp-units/utility/uncertain.h b/src/utility/include/mp-units/utility/uncertain.h index ff3a03860e..f5c9563fde 100644 --- a/src/utility/include/mp-units/utility/uncertain.h +++ b/src/utility/include/mp-units/utility/uncertain.h @@ -674,9 +674,10 @@ struct std::common_type> { * * A `~` appended to that spec selects instead the concise notation of ISO 80000-1:2022, * 7.2.4, in which the value is quoted to the last significant digit of the uncertainty and - * the uncertainty follows in parentheses, counted in units of that digit. The tilde marks - * the output as an approximation: the default form prints the value as it is, while this one - * rounds it to the precision the uncertainty justifies: + * the uncertainty follows in parentheses, counted in units of that digit. Where the default + * form prints both components losslessly, this one quotes the value to the precision the + * uncertainty justifies (GUM 7.2.6); for a constant transcribed from a metrology table it + * reproduces the published notation verbatim: * * @code * uncertain val{6.67430e-11, 1.5e-15}; diff --git a/test/runtime/uncertain_test.cpp b/test/runtime/uncertain_test.cpp index a6fbd08be5..3604345806 100644 --- a/test/runtime/uncertain_test.cpp +++ b/test/runtime/uncertain_test.cpp @@ -286,9 +286,10 @@ TEST_CASE("measurement_of a measured constant", "[uncertain]") SECTION("conversion to SI materializes the relative uncertainty") { + // CODATA 2018: G = 6.674 30(15) x 10^-11, so u_r derives as the exact ratio of the two const quantity in_si = G_measured.in(m3 / kg / s2); CHECK_THAT(in_si.numerical_value_in(m3 / kg / s2).value(), WithinRel(6.6743e-11)); - CHECK_THAT(in_si.numerical_value_in(m3 / kg / s2).relative_uncertainty(), WithinRel(2.2e-5)); + CHECK_THAT(in_si.numerical_value_in(m3 / kg / s2).relative_uncertainty(), WithinRel(1.5e-15 / 6.6743e-11)); } SECTION("cancellation against the constant unit stays symbolic and exact") @@ -304,7 +305,7 @@ TEST_CASE("measurement_of a measured constant", "[uncertain]") { const quantity solar_mass = (1.0 * iau::nominal_solar_mass_parameter) / G_measured; CHECK_THAT(solar_mass.numerical_value_in(kg).value(), WithinRel(1.98841e30, 1e-4)); - CHECK_THAT(solar_mass.numerical_value_in(kg).relative_uncertainty(), WithinRel(2.2e-5)); + CHECK_THAT(solar_mass.numerical_value_in(kg).relative_uncertainty(), WithinRel(1.5e-15 / 6.6743e-11)); } SECTION("a unit defined from a measured constant embeds its central value") @@ -327,22 +328,22 @@ TEST_CASE("conversion factors built from measured constants carry their uncertai const quantity in_kg = value_cast>(mass); CHECK_THAT(in_kg.numerical_value_in(kg).value(), WithinRel(3.97682e30, 1e-4)); // solar mass = (GM)_sun/G, so the kg conversion inherits exactly G's relative uncertainty - CHECK_THAT(in_kg.numerical_value_in(kg).relative_uncertainty(), WithinRel(2.2e-5)); + CHECK_THAT(in_kg.numerical_value_in(kg).relative_uncertainty(), WithinRel(1.5e-15 / 6.6743e-11)); } SECTION("in(unit) spells the same opt-in") { const quantity in_kg = mass.in>(kg); CHECK_THAT(in_kg.numerical_value_in(kg).value(), WithinRel(3.97682e30, 1e-4)); - CHECK_THAT(in_kg.numerical_value_in(kg).relative_uncertainty(), WithinRel(2.2e-5)); + CHECK_THAT(in_kg.numerical_value_in(kg).relative_uncertainty(), WithinRel(1.5e-15 / 6.6743e-11)); } SECTION("an uncertain representation folds the factor in on a plain in(unit)") { const quantity counted = uncertain{2.0, 0.1} * iau::solar_mass; const quantity in_kg = counted.in(kg); - // the value's own 5% and the factor's 2.2e-5 combine in quadrature - CHECK_THAT(in_kg.numerical_value_in(kg).relative_uncertainty(), WithinRel(std::hypot(0.05, 2.2e-5))); + // the value's own 5% and the factor's u_r(G) combine in quadrature + CHECK_THAT(in_kg.numerical_value_in(kg).relative_uncertainty(), WithinRel(std::hypot(0.05, 1.5e-15 / 6.6743e-11))); } SECTION("a shared measured constant cancels, exactly as its central value does") @@ -360,14 +361,16 @@ TEST_CASE("conversion factors built from measured constants carry their uncertai { const quantity in_kg = uncertain{3.97682e30} * kg; const quantity in_solar_masses = in_kg.in(iau::solar_mass); - CHECK_THAT(in_solar_masses.numerical_value_in(iau::solar_mass).relative_uncertainty(), WithinRel(2.2e-5)); + CHECK_THAT(in_solar_masses.numerical_value_in(iau::solar_mass).relative_uncertainty(), + WithinRel(1.5e-15 / 6.6743e-11)); } SECTION("a fractional power of a measured constant scales its uncertainty contribution") { const quantity q = uncertain{1.0} * sqrt(iau::newtonian_constant_of_gravitation); const quantity converted = q.in(sqrt(m3 / kg / s2)); - CHECK_THAT(converted.numerical_value_in(sqrt(m3 / kg / s2)).relative_uncertainty(), WithinRel(0.5 * 2.2e-5)); + CHECK_THAT(converted.numerical_value_in(sqrt(m3 / kg / s2)).relative_uncertainty(), + WithinRel(0.5 * (1.5e-15 / 6.6743e-11))); } SECTION("an exact representation type stays exact") @@ -384,7 +387,7 @@ TEST_CASE("conversion factors built from measured constants carry their uncertai const quantity solar = uncertain{1.0} * iau::solar_mass; const quantity in_kg = uncertain{1.0e30} * kg; const quantity sum = solar + in_kg; - const auto expected_sigma = 1.98841e30 * 2.2e-5; + const auto expected_sigma = 1.98841e30 * (1.5e-15 / 6.6743e-11); CHECK_THAT(sum.numerical_value_in(kg).value(), WithinRel(2.98841e30, 1e-4)); CHECK_THAT(sum.numerical_value_in(kg).uncertainty(), WithinRel(expected_sigma, 1e-3)); } @@ -396,11 +399,13 @@ TEST_CASE("SI CODATA constants carry their adjustment's uncertainty", "[uncertai // Since the redefinition it is measured and departs from that in the tenth digit. const quantity mu = measurement_of(codata::magnetic_constant).in(H / m); CHECK_THAT(mu.numerical_value_in(H / m).value(), WithinRel(1.25663706127e-6)); - CHECK_THAT(mu.numerical_value_in(H / m).relative_uncertainty(), WithinRel(1.6e-10)); + // CODATA 2022 publishes 1.256 637 061 27(20) x 10^-6, and the sigma folded into the value is + // exactly the published one, so u_r comes out as their ratio + CHECK_THAT(mu.numerical_value_in(H / m).relative_uncertainty(), WithinRel(2.0e-16 / 1.25663706127e-6)); std::ostringstream os; os << mu; - CHECK(os.str() == "1.25664e-06 ± 2.01062e-16 H/m"); + CHECK(os.str() == "1.25664e-06 ± 2e-16 H/m"); } TEST_CASE("uncertain text output", "[uncertain]") diff --git a/test/static/uncertain_test.cpp b/test/static/uncertain_test.cpp index b4bfb60efe..6ba69c3941 100644 --- a/test/static/uncertain_test.cpp +++ b/test/static/uncertain_test.cpp @@ -82,18 +82,22 @@ static_assert(std::is_same_v() * std::de static_assert(std::is_same_v() + std::declval()), quantity>>); -// only constants defined with `relative_standard_uncertainty` are measured constants +// only constants defined with an uncertainty wrapper (`relative_standard_uncertainty` or +// `standard_uncertainty`) are measured constants static_assert(MeasuredConstant>); static_assert(!MeasuredConstant>); static_assert(!MeasuredConstant>); static_assert(!MeasuredConstant>); -// A published `u_r` is a two-significant-digit decimal, and materializing it into a floating-point -// type from an exact magnitude need not reproduce the literal bit for bit on every platform, so -// these are compared with `approx_equal` rather than with `==`. -// CODATA 2018: G = 6.674 30(15) × 10⁻¹¹ m³ kg⁻¹ s⁻², u_r = 2.2 × 10⁻⁵ -static_assert(approx_equal( - get_value(get_relative_standard_uncertainty(iau::newtonian_constant_of_gravitation)) * one, 2.2e-5 * one)); +// CODATA 2018: G = 6.674 30(15) × 10⁻¹¹ m³ kg⁻¹ s⁻². The definition stores the published absolute +// uncertainty, and the relative form derives from it as an exact ratio of canonical magnitudes: +// u_r = 1.5e-15 / 6.6743e-11 = 15/66743 × 10⁻¹, so it can be compared exactly. +static_assert(get_relative_standard_uncertainty(iau::newtonian_constant_of_gravitation) == + mag_ratio<15, 66'743> * mag_power<10, -1>); + +// the stored absolute form is returned verbatim +static_assert(get_standard_uncertainty(iau::newtonian_constant_of_gravitation) == + mag_ratio<15, 10> * mag_power<10, -15> * cubic(si::metre) / si::kilogram / square(si::second)); // `measurement_of` returns exactly 1 of the constant in an uncertainty-capable representation; // the uncertainty is relational and materializes only when a conversion leaves units in which @@ -121,19 +125,26 @@ static_assert(has_relative_standard_uncertainty); // HEP measured constants carry the uncertainty published by their own CODATA release, so the same -// constant has a different one per namespace +// constant has a different one per namespace. The derived `u_r` is the exact ratio of the two +// published magnitudes; materializing it into a floating-point type need not reproduce a decimal +// literal bit for bit on every platform, so these are compared with `approx_equal`. static_assert(MeasuredConstant>); static_assert(MeasuredConstant>); +// CODATA 2018: 0.510 998 950 00(15) MeV/c² static_assert(approx_equal(get_value(get_relative_standard_uncertainty(hep::codata2018::electron_mass)) * one, - 3.0e-10 * one)); + 1.5e-10 / 0.51099895 * one)); +// CODATA 2022: 0.510 998 950 69(16) MeV/c² static_assert(approx_equal(get_value(get_relative_standard_uncertainty(hep::codata2022::electron_mass)) * one, - 3.1e-10 * one)); + 1.6e-10 / 0.51099895069 * one)); +// CODATA 2014: 0.510 998 9461(31) MeV/c² static_assert(approx_equal(get_value(get_relative_standard_uncertainty(hep::codata2014::electron_mass)) * one, - 6.2e-9 * one)); + 3.1e-9 / 0.5109989461 * one)); // the fine-structure constant is dimensionless, so its uncertainty is still ordinary metadata -static_assert(approx_equal( - get_value(get_relative_standard_uncertainty(hep::codata2022::fine_structure_constant)) * one, 1.6e-10 * one)); +// (CODATA 2022: 7.297 352 5643(11) × 10⁻³) +static_assert( + approx_equal(get_value(get_relative_standard_uncertainty(hep::codata2022::fine_structure_constant)) * one, + 1.1e-12 / 7.2973525643e-3 * one)); // k_B is exact since the 2019 SI redefinition, so the post-2019 releases carry no uncertainty while // the pre-2019 one does @@ -145,10 +156,13 @@ static_assert(!MeasuredConstant>); static_assert(MeasuredConstant>); static_assert(MeasuredConstant>); -static_assert(approx_equal( - get_value(get_relative_standard_uncertainty(hep::codata2018::permeability_of_vacuum)) * one, 1.5e-10 * one)); -static_assert(approx_equal( - get_value(get_relative_standard_uncertainty(hep::codata2022::permeability_of_vacuum)) * one, 1.6e-10 * one)); +// CODATA 2018: 1.256 637 062 12(19) × 10⁻⁶ N/A²; CODATA 2022: 1.256 637 061 27(20) × 10⁻⁶ N/A² +static_assert( + approx_equal(get_value(get_relative_standard_uncertainty(hep::codata2018::permeability_of_vacuum)) * one, + 1.9e-16 / 1.25663706212e-6 * one)); +static_assert( + approx_equal(get_value(get_relative_standard_uncertainty(hep::codata2022::permeability_of_vacuum)) * one, + 2.0e-16 / 1.25663706127e-6 * one)); // The SI-expressed values track the same history. They are separate CODATA table entries from the // HEP ones above: `hep` declares its own system of quantities, so those constants are not even the @@ -158,10 +172,10 @@ static_assert(MeasuredConstant>); static_assert(approx_equal(get_value(get_relative_standard_uncertainty(codata::codata2018::magnetic_constant)) * one, - 1.5e-10 * one)); + 1.9e-16 / 1.25663706212e-6 * one)); static_assert(approx_equal(get_value(get_relative_standard_uncertainty(codata::codata2022::magnetic_constant)) * one, - 1.6e-10 * one)); + 2.0e-16 / 1.25663706127e-6 * one)); // The newest adjustment is `inline`, so the unqualified name is the current value. `si` keeps a // deprecated `magnetic_constant` holding the pre-2019 exact value (not named here, since referring @@ -188,9 +202,10 @@ static_assert(!MeasuredConstant kg) is exactly G's contribution: (GM)ᴺ is exact, |−1| · u_r(G) -static_assert(approx_equal(detail::conversion_relative_uncertainty(iau::solar_mass, si::kilogram) * one, 2.2e-5L * one, - 1e-6)); +// u_r(solar_mass -> kg) is exactly G's contribution: (GM)ᴺ is exact, |−1| · u_r(G), where +// u_r(G) derives from the published absolute uncertainty as 1.5e-15 / 6.6743e-11 +static_assert(approx_equal(detail::conversion_relative_uncertainty(iau::solar_mass, si::kilogram) * one, + 1.5e-15L / 6.6743e-11L * one, 1e-6)); // a constant shared by both units cancels symbolically: (GM)☉ᴺ/G vs (GM)⊕ᴺ/G static_assert(detail::conversion_relative_uncertainty(iau::solar_mass, iau::terrestrial_mass) == 0.0L); @@ -207,7 +222,7 @@ static_assert(approx_equal(detail::conversion_relative_uncertainty(sqrt(iau::new sqrt(cubic(si::metre) / si::kilogram / square(si::second))) * one, - 0.5L * 2.2e-5L * one, 1e-6)); + 0.5L * (1.5e-15L / 6.6743e-11L) * one, 1e-6)); // local constants exercising the tree-walk rules directly inline constexpr struct measured_base final : @@ -241,4 +256,56 @@ static_assert(detail::conversion_relative_uncertainty(measured_base / measured_b } // namespace conversion_uncertainty +// A constant may declare its uncertainty in either the relative or the absolute form, whichever +// its source publishes, and each accessor derives the missing form on demand. +namespace uncertainty_wrappers { + +// the absolute form derives the relative one for the RSU-declared constant above: +// u(x) = u_r · |x| = 1e-3 · 2 m = 2e-3 m +static_assert(equivalent(get_standard_uncertainty(conversion_uncertainty::measured_base), + mag_ratio<2, 1000>* si::metre)); + +// electron magnetic moment, CODATA 2022: −9.284 764 6917(29) × 10⁻²⁴ J/T; a negative-valued +// constant declared with the absolute form +inline constexpr struct electron_magnetic_moment final : + named_constant * mag_power<10, -24> * si::joule / si::tesla, + standard_uncertainty{mag_ratio<29, 10> * mag_power<10, -33> * si::joule / si::tesla}> { +} electron_magnetic_moment; + +static_assert(MeasuredConstant>); + +// the declared uncertainty is returned verbatim and is positive by construction +static_assert(get_standard_uncertainty(electron_magnetic_moment) == + mag_ratio<29, 10> * mag_power<10, -33> * si::joule / si::tesla); + +// `u_r = u(x)/|x|` is a magnitude, never negative (GUM/VIM), and derives as the exact ratio +// 2.9e-33 / 9.2847646917e-24 = 29/92'847'646'917 +static_assert(get_relative_standard_uncertainty(electron_magnetic_moment) == mag_ratio<29, 92'847'646'917>); +static_assert(detail::magnitude_is_positive); + +// a negative-valued constant declared with the relative form derives a positive absolute one: +// u(x) = u_r · |x| = 1e-3 · 2 m = 2e-3 m +inline constexpr struct negative_measured final : + named_constant<"tnm", -mag<2> * si::metre, relative_standard_uncertainty{mag_ratio<1, 1> * mag_power<10, -3>}> { +} negative_measured; +static_assert(equivalent(get_standard_uncertainty(negative_measured), mag_ratio<2, 1000>* si::metre)); +static_assert(detail::magnitude_is_positive); + +// the conversion engine folds the uncertainty of an absolute-form constant exactly as it does +// for a relative-form one +static_assert(approx_equal(detail::conversion_relative_uncertainty(electron_magnetic_moment, si::joule / si::tesla) * + one, + 2.9e-33L / 9.2847646917e-24L * one, 1e-6)); + +// an uncertainty of a different dimension than the constant, or a non-positive one, is not a +// valid definition +template +concept valid_measured_constant = requires { named_constant<"test", U, standard_uncertainty{SU}>{}; }; +static_assert(valid_measured_constant * si::metre, mag_ratio<1, 1000> * si::metre>); +static_assert(!valid_measured_constant * si::metre, mag_ratio<1, 1000> * si::second>); +static_assert(!valid_measured_constant * si::metre, -mag_ratio<1, 1000> * si::metre>); + +} // namespace uncertainty_wrappers + } // namespace From 1e9b4f74ecdda1817ba8ddcb6456bcc7eb792b3d Mon Sep 17 00:00:00 2001 From: Mateusz Pusz Date: Thu, 6 Aug 2026 13:11:08 +0200 Subject: [PATCH 03/10] feat: compile-time factorization via double-wide `mul_mod` and batched-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 --- CHANGELOG.md | 6 ++ src/core/include/mp-units/ext/prime.h | 133 +++++++++++++++++++++++++- test/static/prime_test.cpp | 21 ++++ 3 files changed, 156 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef997ccaca..b6af103c99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,12 @@ This page documents the version history and changes for the **mp-units** library - fix: `uncertain::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 - (!) 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, diff --git a/src/core/include/mp-units/ext/prime.h b/src/core/include/mp-units/ext/prime.h index 5a109d7c62..c25e1ff601 100644 --- a/src/core/include/mp-units/ext/prime.h +++ b/src/core/include/mp-units/ext/prime.h @@ -81,12 +81,17 @@ namespace mp_units::detail { } } -// (a * b) % n. +// (a * b) % n, reducing an overflowing product by recursive chunking. +// +// This is the portable fallback for compilers without a 128-bit integer type. The recursion is +// bounded, but costs enough constexpr steps that the double-wide `mul_mod` below is strongly +// preferred where available (`mul_mod` is the innermost operation of both the Baillie-PSW test +// and Pollard's rho, so its cost multiplies through everything). // // Precondition: (a < n). // Precondition: (b < n). // Precondition: (n > 0). -[[nodiscard]] consteval std::uint64_t mul_mod(std::uint64_t a, std::uint64_t b, std::uint64_t n) +[[nodiscard]] consteval std::uint64_t mul_mod_via_chunking(std::uint64_t a, std::uint64_t b, std::uint64_t n) { MP_UNITS_PRECONDITION_DEBUG(a < n); MP_UNITS_PRECONDITION_DEBUG(b < n); @@ -102,7 +107,7 @@ namespace mp_units::detail { return add_mod( // Transform into "negative space" to make the first parameter as small as possible; // then, transform back. - (n - mul_mod(n % a, num_batches, n)) % n, + (n - mul_mod_via_chunking(n % a, num_batches, n)) % n, // Handle the leftover product (which is guaranteed to fit in the integer type). (a * (b % batch_size)) % n, @@ -110,6 +115,28 @@ namespace mp_units::detail { n); } +// (a * b) % n. +// +// Precondition: (a < n). +// Precondition: (b < n). +// Precondition: (n > 0). +[[nodiscard]] consteval std::uint64_t mul_mod(std::uint64_t a, std::uint64_t b, std::uint64_t n) +{ + MP_UNITS_PRECONDITION_DEBUG(a < n); + MP_UNITS_PRECONDITION_DEBUG(b < n); + MP_UNITS_PRECONDITION_DEBUG(n > 0u); + +#if defined(__SIZEOF_INT128__) + // forming the full-width product and reducing it in one step costs a small constant number of + // constexpr steps, an order of magnitude less than the chunking fallback (see the analysis in + // https://github.com/aurora-opensource/au/pull/686, which this mirrors) + __extension__ using uint128 = unsigned __int128; + return static_cast(static_cast(a) * b % n); +#else + return mul_mod_via_chunking(a, b, n); +#endif +} + // (a / 2) % n. // // Precondition: (a < n). @@ -446,6 +473,95 @@ constexpr auto get_first_of(const Rng& rng, UnaryFunction f) template constexpr auto first_n_primes_result = first_n_primes(); +// One run of Pollard's rho on the polynomial `x^2 + c (mod n)`, with Brent's cycle detection and +// batched gcd checks: the |tortoise - hare| differences are accumulated as a running product +// modulo `n`, and a single gcd is taken per batch, with a one-step replay from the batch start +// when the batch overshoots (accumulates a multiple of every factor). Expected cost is O(n^(1/4)) +// modular multiplications. Returns a non-trivial factor of `n`, or `n` itself when this +// parameterization fails (the caller then tries another `c`). Mirrors the design validated in Au +// (https://github.com/aurora-opensource/au/pull/686). +// +// `x^2 + c (mod n)`: the iterated polynomial of Pollard's rho. +[[nodiscard]] consteval std::uint64_t rho_advance(std::uint64_t x, std::uint64_t n, std::uint64_t c) +{ + return add_mod(mul_mod(x, x, n), c, n); +} + +[[nodiscard]] consteval std::uint64_t rho_distance(std::uint64_t a, std::uint64_t b) { return a > b ? a - b : b - a; } + +// Precondition: `n` is odd and composite. +[[nodiscard]] consteval std::uint64_t pollard_rho_attempt(std::uint64_t n, std::uint64_t c) +{ + MP_UNITS_PRECONDITION_DEBUG(n % 2u == 1u); + MP_UNITS_PRECONDITION_DEBUG(c < n); + + constexpr std::uint64_t batch_size = 128u; + + std::uint64_t tortoise = 2u; // the anchor of the current power-of-two segment + std::uint64_t hare = 2u; // the moving point + std::uint64_t backtrack = 2u; // the hare's position at the start of the current batch + std::uint64_t factor = 1u; + + for (std::uint64_t segment_length = 1u; factor == 1u; segment_length *= 2u) { + tortoise = hare; + for (std::uint64_t i = 0u; i < segment_length; ++i) { + hare = rho_advance(hare, n, c); + } + for (std::uint64_t done = 0u; done < segment_length && factor == 1u; done += batch_size) { + backtrack = hare; + std::uint64_t product = 1u; + const std::uint64_t steps = segment_length - done < batch_size ? segment_length - done : batch_size; + for (std::uint64_t i = 0u; i < steps; ++i) { + hare = rho_advance(hare, n, c); + product = mul_mod(product, rho_distance(tortoise, hare), n); + } + factor = std::gcd(product, n); + } + } + + if (factor == n) { + // the batch overshot (its product collected a multiple of every prime factor); replay it one + // step at a time to catch a factor in isolation + do { + backtrack = rho_advance(backtrack, n, c); + factor = std::gcd(rho_distance(tortoise, backtrack), n); + } while (factor == 1u); + } + return factor; +} + +// The smallest prime factor of a composite `n` all of whose prime factors exceed the +// trial-division cutoff of `find_first_factor` below. Pollard's rho splits `n` recursively, with +// Baillie-PSW certifying the leaves; the smallest leaf is the answer. Returns `std::nullopt` in +// the (never yet observed) case that every rho parameterization fails for some split. +// The two functions are mutually recursive free functions rather than a local lambda: only C++23 +// consteval propagation would let a plain lambda call `consteval` functions with its parameter. +[[nodiscard]] consteval std::optional smallest_prime_factor_of_hard_composite(std::uint64_t n); + +// `m` itself when it is prime, otherwise its smallest prime factor found via Pollard's rho. +[[nodiscard]] consteval std::optional smallest_prime_in(std::uint64_t m) +{ + return baillie_psw_probable_prime(m) ? std::optional{m} : smallest_prime_factor_of_hard_composite(m); +} + +[[nodiscard]] consteval std::optional smallest_prime_factor_of_hard_composite(std::uint64_t n) +{ + std::uint64_t factor = n; + for (std::uint64_t c = 1u; factor == n; ++c) { + if (c > 100u) { + return std::nullopt; + } + factor = pollard_rho_attempt(n, c); + } + + const auto lhs = smallest_prime_in(factor); + const auto rhs = smallest_prime_in(n / factor); + if (!lhs || !rhs) { + return std::nullopt; + } + return *lhs < *rhs ? *lhs : *rhs; +} + [[nodiscard]] consteval std::uintmax_t find_first_factor(std::uintmax_t n) { constexpr auto first_100_primes = first_n_primes_result<100>; @@ -464,7 +580,16 @@ constexpr auto first_n_primes_result = first_n_primes(); return n; } - // If we're here, we know `n` is composite, so continue with trial division for all odd numbers. + // `n` is composite with every prime factor above the trial-division range, so Pollard's rho + // finds one in ~n^(1/4) steps where trial division needs up to ~n^(1/2) iterations and can + // exceed compiler constexpr-loop limits (a semiprime of two ~500'000-sized primes needs ~266k + // iterations, over GCC's default limit of 262144; hit by real CODATA values). + if (const auto factor = smallest_prime_factor_of_hard_composite(n)) { + return *factor; + } + + // Pollard's rho failed on every tried parameterization (not observed in practice for any + // 64-bit input); fall back to the robust trial division over all odd numbers. std::uintmax_t factor = first_100_primes.back() + 2u; while (factor * factor <= n) { if (n % factor == 0u) { diff --git a/test/static/prime_test.cpp b/test/static/prime_test.cpp index 2ec67abf20..66f4b16b4f 100644 --- a/test/static/prime_test.cpp +++ b/test/static/prime_test.cpp @@ -176,4 +176,25 @@ static_assert(baillie_psw_probable_prime(9'007'199'254'740'881u), "Large known p static_assert(baillie_psw_probable_prime(18'446'744'073'709'551'557u), "Largest 64-bit prime"); +// The double-wide `mul_mod` and its portable chunking fallback must agree, including on the +// operand ranges where the naive product overflows. +static_assert(mul_mod_via_chunking(6u, 7u, 10u) == mul_mod(6u, 7u, 10u)); +static_assert(mul_mod_via_chunking(MAX_U64 / 2u, 10u, MAX_U64) == mul_mod(MAX_U64 / 2u, 10u, MAX_U64)); +static_assert(mul_mod_via_chunking(9'223'372'036'854'775'807u, 9'223'372'036'854'775'806u, MAX_U64) == + mul_mod(9'223'372'036'854'775'807u, 9'223'372'036'854'775'806u, MAX_U64)); + +// `find_first_factor` returns the smallest prime factor. The interesting inputs are composites +// whose factors all exceed the trial-division range, where Pollard's rho does the finding: +// unbounded trial division would need ~266k `constexpr` iterations for the first one, more than +// GCC's default `-fconstexpr-loop-limit` (hit by the CODATA 2018 atomic unit of electric +// potential, 27.211 386 245 988 V). +static_assert(find_first_factor(323'945'074'357u) == 531'871u, "Semiprime of two ~5*10^5 primes"); +static_assert(find_first_factor(614'889'782'588'491'410u) == 2u, "Primorial: smallest factor first"); +static_assert(find_first_factor(3'233u * 4'933u) == 53u, "53 * 61 * 4933: smallest prime returned"); +static_assert(find_first_factor(225'653'407'801u) == 225'653'407'801u, "Large prime returns itself"); +static_assert(find_first_factor(334'524'384'739u * 2u) == 2u, "Small factor found by trial division"); +static_assert(find_first_factor(600'851'475'143u) == 71u, "Composite with several mid-size factors"); +static_assert(find_first_factor(1'000'036'000'099u) == 1'000'003u, // 1'000'003 * 1'000'033 + "Smallest prime reported even when rho may split off the larger one first"); + } // namespace From 369d3ce2e7fcecf8893384b365822d11eeb424b7 Mon Sep 17 00:00:00 2001 From: Mateusz Pusz Date: Thu, 6 Aug 2026 13:11:26 +0200 Subject: [PATCH 04/10] perf: the whole `unit_magnitude` interface hosted as hidden friends of 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 --- CHANGELOG.md | 9 + .../include/mp-units/bits/unit_magnitude.h | 626 ++++++++++-------- .../mp-units/framework/unit_definitions.h | 3 +- 3 files changed, 350 insertions(+), 288 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6af103c99..642ab8dc6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,15 @@ This page documents the version history and changes for the **mp-units** library 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, diff --git a/src/core/include/mp-units/bits/unit_magnitude.h b/src/core/include/mp-units/bits/unit_magnitude.h index 80490c5a34..496c4a4b4a 100644 --- a/src/core/include/mp-units/bits/unit_magnitude.h +++ b/src/core/include/mp-units/bits/unit_magnitude.h @@ -250,118 +250,22 @@ consteval void try_accumulate_element(std::optional& result) return static_cast(get_base_value(lhs)) < static_cast(get_base_value(rhs)); } -// The largest integer which can be extracted from any magnitude with only a single basis vector. -template -[[nodiscard]] consteval auto integer_part(unit_magnitude); [[nodiscard]] consteval std::intmax_t integer_part(ratio r) { return r.num / r.den; } -template -[[nodiscard]] consteval auto remove_positive_power(unit_magnitude m); -template -[[nodiscard]] consteval auto remove_mag_constants(unit_magnitude m); -template -[[nodiscard]] consteval auto only_positive_mag_constants(unit_magnitude m); -template -[[nodiscard]] consteval auto only_negative_mag_constants(unit_magnitude m); - template requires is_positive_mag_arg [[nodiscard]] consteval UnitMagnitude auto mag_power_lazy(); -// Forward declarations; fully defined after unit_magnitude -template -[[nodiscard]] consteval auto abs_magnitude(unit_magnitude); -[[nodiscard]] consteval auto abs_magnitude(unit_magnitude<>); - -template -[[nodiscard]] consteval auto pow_magnitude(unit_magnitude<>); -template -[[nodiscard]] consteval auto pow_magnitude(unit_magnitude); - -template -struct magnitude_base {}; - -template -struct magnitude_base> { - template - [[nodiscard]] friend consteval UnitMagnitude auto multiply_impl(unit_magnitude, unit_magnitude) - { - if constexpr (mag_less(H, H2)) { - if constexpr (sizeof...(T) == 0) { - // Shortcut for the "pure prepend" case, which makes it easier to implement some of the other cases. - return unit_magnitude{}; - } else { - return unit_magnitude{} * (unit_magnitude{} * unit_magnitude{}); - } - } else if constexpr (mag_less(H2, H)) { - return unit_magnitude

{} * (unit_magnitude{} * unit_magnitude{}); - } else { - if constexpr (std::is_same_v) { - constexpr auto partial_product = unit_magnitude{} * unit_magnitude{}; - if constexpr (is_negative_tag) { - // (-1) * (-1) = 1: two negatives cancel each other out - return partial_product; - } else if constexpr (get_exponent(H) + get_exponent(H2) == 0) { - return partial_product; - } else { - // Make a new power_v with the common base of H and H2, whose power is their powers' sum. - constexpr auto new_head = power_v_or_T(); - - if constexpr (get_exponent(new_head) == 0) { - return partial_product; - } else { - return unit_magnitude{} * partial_product; - } - } - } - } - } - - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // Common Magnitude. - // - // The "common Magnitude" C, of two Magnitudes M1 and M2, is the largest Magnitude such that each of its inputs is - // expressible by only positive basis powers relative to C. That is, both (M1 / C) and (M2 / C) contain only positive - // powers in the expansion on our basis. - // - // For rational Magnitudes (or, more precisely, Magnitudes that are rational _relative to each other_), this reduces - // to the familiar convention from the std::chrono library: it is the largest Magnitude C such that each input - // Magnitude is an _integer multiple_ of C. The connection can be seen by considering the definition in the above - // paragraph, and recognizing that both the bases and the powers are all integers for rational Magnitudes. - // - // For relatively _irrational_ Magnitudes (whether from irrational bases, or fractional powers of integer bases), the - // notion of a "common type" becomes less important, because there is no way to preserve pure integer multiplication. - // When we go to retrieve our value, we'll be stuck with a floating point approximation no matter what choice we make. - // Thus, we make the _simplest_ choice which reproduces the correct convention in the rational case: namely, taking - // the minimum power for each base (where absent bases implicitly have a power of 0). - template - [[nodiscard]] friend consteval auto common_magnitude(unit_magnitude, unit_magnitude) - { - if constexpr (get_base_value(H) < get_base_value(H2)) { - // When H1 has the smaller base, prepend to result from recursion. - return mp_units::detail::remove_positive_power(unit_magnitude{}) * - common_magnitude(unit_magnitude{}, unit_magnitude{}); - } else if constexpr (get_base_value(H2) < get_base_value(H)) { - // When H2 has the smaller base, prepend to result from recursion. - return mp_units::detail::remove_positive_power(unit_magnitude

{}) * - common_magnitude(unit_magnitude{}, unit_magnitude{}); - } else { - // When the bases are equal, pick whichever has the lower power. - constexpr auto common_tail = common_magnitude(unit_magnitude{}, unit_magnitude{}); - if constexpr (get_exponent(H) < get_exponent(H2)) { - return unit_magnitude{} * common_tail; - } else { - return unit_magnitude

{} * common_tail; - } - } - } -}; +// The two escape hatches of `unit_magnitude_interface` below. Friend bodies in the interface may +// not name a concrete specialization of the still-incomplete `unit_magnitude` in a non-dependent +// expression, and these two need to (`unit_magnitude<>`, `unit_magnitude`), so +// they are the only magnitude-taking functions living at namespace scope; the dependent calls +// resolve here at instantiation, when the class is complete. +template +[[nodiscard]] consteval unit_magnitude<> empty_magnitude(unit_magnitude); template -[[nodiscard]] consteval std::size_t magnitude_list_size(unit_magnitude) -{ - return sizeof...(Ms); -} +[[nodiscard]] consteval auto negate_magnitude(unit_magnitude); template Out> [[nodiscard]] constexpr Out print_separator(Out out, const unit_symbol_formatting& fmt) @@ -378,25 +282,6 @@ template Out> return out; } -template Out, auto... Ms> - requires(sizeof...(Ms) == 0) -[[nodiscard]] constexpr auto mag_constants_text(Out out, unit_magnitude, const unit_symbol_formatting&, bool) -{ - return out; -} - -template Out, auto M, auto... Rest> -[[nodiscard]] constexpr auto mag_constants_text(Out out, unit_magnitude, const unit_symbol_formatting& fmt, - bool negative_power) -{ - auto to_symbol = [&](T v) { - out = copy_symbol(get_base(v)._symbol_, fmt.char_set, negative_power, out); - constexpr ratio r = get_exponent(T{}); - return copy_symbol_exponent(fmt.char_set, negative_power, out); - }; - return (to_symbol(M), ..., (out = print_separator(out, fmt), to_symbol(Rest))); -} - template Out> [[nodiscard]] constexpr Out magnitude_symbol_impl(Out out, const unit_symbol_formatting& fmt) @@ -461,49 +346,69 @@ template