diff --git a/.flake8 b/.flake8
index 4592939f20..1404045659 100644
--- a/.flake8
+++ b/.flake8
@@ -7,3 +7,6 @@ ignore =
E712,
# line break before binary operator
W503
+per-file-ignores =
+ # flake8 is just plain wrong here, contradicting black
+ .github/generate-job-matrix.py:E225,E231
diff --git a/.github/generate-job-matrix.py b/.github/generate-job-matrix.py
new file mode 100644
index 0000000000..bd14ee0000
--- /dev/null
+++ b/.github/generate-job-matrix.py
@@ -0,0 +1,206 @@
+import argparse
+import json
+import os
+import random
+import typing
+from types import SimpleNamespace
+
+from job_matrix import CombinationCollector, Compiler, Configuration
+
+
+def make_gcc_config(version: int) -> Configuration:
+ return Configuration(
+ name=f"GCC-{version}",
+ os="ubuntu-24.04",
+ compiler=Compiler(
+ type="GCC",
+ version=version,
+ cc=f"gcc-{version}",
+ cxx=f"g++-{version}",
+ ),
+ cxx_modules=False,
+ std_format_support=version >= 13,
+ )
+
+
+def make_clang_config(
+ version: int, platform: typing.Literal["x86-64", "arm64"] = "x86-64"
+) -> Configuration:
+ cfg = SimpleNamespace(
+ name=f"Clang-{version} ({platform})",
+ compiler=SimpleNamespace(
+ type="CLANG",
+ version=version,
+ ),
+ lib="libc++",
+ cxx_modules=version >= 17,
+ std_format_support=version >= 17,
+ )
+ match platform:
+ case "x86-64":
+ cfg.os = "ubuntu-22.04" if version < 17 else "ubuntu-24.04"
+ cfg.compiler.cc = f"clang-{version}"
+ cfg.compiler.cxx = f"clang++-{version}"
+ case "arm64":
+ cfg.os = "macos-14"
+ pfx = f"/opt/homebrew/opt/llvm@{version}/bin"
+ cfg.compiler.cc = f"{pfx}/clang"
+ cfg.compiler.cxx = f"{pfx}/clang++"
+ case _:
+ raise KeyError(f"Unsupported platform {platform!r} for Clang")
+ ret = cfg
+ ret.compiler = Compiler(**vars(cfg.compiler))
+ return Configuration(**vars(ret))
+
+
+def make_apple_clang_config(version: int) -> Configuration:
+ ret = Configuration(
+ name=f"Apple Clang {version}",
+ os="macos-13",
+ compiler=Compiler(
+ type="APPLE_CLANG",
+ version=f"{version}.0",
+ cc="clang",
+ cxx="clang++",
+ ),
+ cxx_modules=False,
+ std_format_support=False,
+ )
+ return ret
+
+
+def make_msvc_config(release: str, version: int) -> Configuration:
+ ret = Configuration(
+ name=f"MSVC {release}",
+ os="windows-2022",
+ compiler=Compiler(
+ type="MSVC",
+ version=version,
+ cc="",
+ cxx="",
+ ),
+ cxx_modules=False,
+ std_format_support=True,
+ )
+ return ret
+
+
+configs = {
+ c.name: c
+ for c in [make_gcc_config(ver) for ver in [12, 13, 14]]
+ + [
+ make_clang_config(ver, platform)
+ for ver in [16, 17, 18]
+ for platform in ["x86-64", "arm64"]
+ # arm64 runners are expensive; only consider one version
+ if ver == 18 or platform != "arm64"
+ ]
+ + [make_apple_clang_config(ver) for ver in [15]]
+ + [make_msvc_config(release="14.4", version=194)]
+}
+
+full_matrix = dict(
+ config=list(configs.values()),
+ std=[20, 23],
+ formatting=["std::format", "fmtlib"],
+ contracts=["none", "gsl-lite", "ms-gsl"],
+ build_type=["Release", "Debug"],
+)
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ # parser.add_argument("-I","--include",nargs="+",action="append")
+ # parser.add_argument("-X","--exclude",nargs="+",action="append")
+ parser.add_argument("--seed", type=int, default=42)
+ parser.add_argument("--preset", default=None)
+ parser.add_argument("--debug", nargs="+", default=["combinations"])
+ parser.add_argument("--suppress-output", default=False, action="store_true")
+
+ args = parser.parse_args()
+
+ rgen = random.Random(args.seed)
+
+ collector = CombinationCollector(
+ full_matrix,
+ hard_excludes=lambda e: (
+ e.formatting == "std::format" and not e.config.std_format_support
+ ),
+ )
+ match args.preset:
+ case None:
+ pass
+ case "all":
+ collector.all_combinations()
+ case "conan" | "cmake":
+ collector.all_combinations(
+ formatting="std::format",
+ contracts="gsl-lite",
+ build_type="Debug",
+ std=20,
+ )
+ collector.all_combinations(
+ filter=lambda me: not me.config.std_format_support,
+ formatting="fmtlib",
+ contracts="gsl-lite",
+ build_type="Debug",
+ std=20,
+ )
+ collector.sample_combinations(rgen=rgen, min_samples_per_value=2)
+ case "clang-tidy":
+ collector.all_combinations(config=configs["Clang-18 (x86-64)"])
+ case "freestanding":
+ # TODO For some reason Clang-18 Debug with -ffreestanding does not pass CMakeTestCXXCompiler
+ collector.all_combinations(
+ filter=lambda e: not (
+ e.config.name.startswith("Clang-18") and e.build_type == "Debug"
+ ),
+ config=[configs[c] for c in ["GCC-14", "Clang-18 (x86-64)"]],
+ contracts="none",
+ std=23,
+ )
+ case _:
+ raise KeyError(f"Unsupported preset {args.preset!r}")
+
+ if not collector.combinations:
+ raise ValueError("No combination has been produced")
+
+ data = sorted(collector.combinations)
+
+ json_data = [e.as_json() for e in data]
+
+ output_file = os.environ.get("GITHUB_OUTPUT")
+ if not args.suppress_output:
+ if output_file:
+ print(f"Writing outputs to {output_file}")
+ with open(output_file, "wt") as fh:
+ fh.write(f"matrix={json.dumps(json_data)}")
+ else:
+ print("No output file received!")
+
+ for dbg in args.debug:
+ match dbg:
+ case "yaml":
+ import yaml
+
+ json_data = json.loads(json.dumps(json_data))
+ print(yaml.safe_dump(json_data))
+ case "json":
+ print(json.dumps(json_data, indent=4))
+ case "combinations":
+ for e in data:
+ print(
+ f"{e.config!s:17s} c++{e.std:2d} {e.formatting:11s} {e.contracts:8s} {e.build_type:8s}"
+ )
+ case "counts":
+ print(f"Total combinations {len(data)}")
+ for (k, v), n in sorted(collector.per_value_counts.items()):
+ print(f" {k}={v}: {n}")
+ case "none":
+ pass
+ case _:
+ raise KeyError(f"Unknown debug mode {dbg!r}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/.github/job_matrix.py b/.github/job_matrix.py
new file mode 100644
index 0000000000..0458a1e9e6
--- /dev/null
+++ b/.github/job_matrix.py
@@ -0,0 +1,139 @@
+import dataclasses
+import itertools
+import random
+import typing
+from dataclasses import dataclass
+
+
+@dataclass(frozen=True, order=True)
+class Compiler:
+ type: typing.Literal["GCC", "CLANG", "APPLE_CLANG", "MSVC"]
+ version: str | int
+ cc: str
+ cxx: str
+
+
+@dataclass(frozen=True, order=True)
+class Configuration:
+ name: str
+ os: str
+ compiler: Compiler
+ cxx_modules: bool
+ std_format_support: bool
+ conan_config: str = ""
+ lib: typing.Literal["libc++", "libstdc++"] | None = None
+
+ def __str__(self):
+ return self.name
+
+
+@dataclass(frozen=True, order=True)
+class MatrixElement:
+ config: Configuration
+ std: typing.Literal[20, 23]
+ formatting: typing.Literal["std::format", "fmtlib"]
+ contracts: typing.Literal["none", "gsl-lite", "ms-gsl"]
+ build_type: typing.Literal["Release", "Debug"]
+
+ def as_json(self):
+ def dataclass_to_json(obj):
+ """Convert dataclasses to something json-serialisable"""
+ if dataclasses.is_dataclass(obj):
+ return {
+ k: dataclass_to_json(v) for k, v in dataclasses.asdict(obj).items()
+ }
+ return obj
+
+ ret = dataclass_to_json(self)
+ # patch boolean conan configuration options
+ config = ret["config"]
+ for k in ["cxx_modules"]:
+ config[k] = "True" if config[k] else "False"
+ return ret
+
+
+class CombinationCollector:
+ """Incremental builder of MatrixElements, allowing successive selection of entries."""
+
+ def __init__(
+ self,
+ full_matrix: dict[str, list[typing.Any]],
+ *,
+ hard_excludes: typing.Callable[[MatrixElement], bool] | None = None,
+ ):
+ self.full_matrix = full_matrix
+ self.hard_excludes = hard_excludes
+ self.combinations: set[MatrixElement] = set()
+ self.per_value_counts: dict[tuple[str, typing.Any], int] = {
+ (k, v): 0 for k, options in full_matrix.items() for v in options
+ }
+
+ def _make_submatrix(self, **overrides):
+ new_matrix = dict(self.full_matrix)
+ for k, v in overrides.items():
+ if not isinstance(v, list):
+ v = [v]
+ new_matrix[k] = v
+ return new_matrix
+
+ def _add_combination(self, e: MatrixElement):
+ if e in self.combinations or (
+ self.hard_excludes is not None and self.hard_excludes(e)
+ ):
+ return
+ self.combinations.add(e)
+ # update per_value_counts
+ for k, v in vars(e).items():
+ idx = (k, v)
+ self.per_value_counts[idx] = self.per_value_counts.get(idx, 0) + 1
+
+ def all_combinations(
+ self,
+ *,
+ filter: typing.Callable[[MatrixElement], bool] | None = None,
+ **overrides,
+ ):
+ """Adds all combinations in the submatrix defined by `overrides`."""
+ matrix = self._make_submatrix(**overrides)
+ keys = tuple(matrix.keys())
+ for combination in itertools.product(*matrix.values()):
+ cand = MatrixElement(**dict(zip(keys, combination)))
+ if filter and not filter(cand):
+ continue
+ self._add_combination(cand)
+
+ def sample_combinations(
+ self,
+ *,
+ rgen: random.Random,
+ min_samples_per_value: int = 1,
+ filter: typing.Callable[[MatrixElement], bool] | None = None,
+ **overrides,
+ ):
+ """Adds samples from the submatrix defined by `overrides`,
+ ensuring each individual value appears at least n times.
+ """
+ matrix = self._make_submatrix(**overrides)
+ missing: dict[tuple[str, typing.Any], int] = {}
+ for key, options in matrix.items():
+ for value in options:
+ idx = (key, value)
+ missing[idx] = min_samples_per_value - self.per_value_counts.get(idx, 0)
+ while missing:
+ (force_key, force_option), remaining = next(iter(missing.items()))
+ if remaining <= 0:
+ missing.pop((force_key, force_option))
+ continue
+ choice = {}
+ for key, options in matrix.items():
+ choice[key] = force_option if key == force_key else rgen.choice(options)
+ cand = MatrixElement(**choice)
+ if filter and not filter(cand):
+ continue
+ self._add_combination(cand)
+ for idx in choice.items():
+ if missing.pop(idx, 0) <= 0:
+ continue
+ remaining = min_samples_per_value - self.per_value_counts.get(idx, 0)
+ if remaining > 0:
+ missing[idx] = remaining
diff --git a/.github/workflows/ci-clang-tidy.yml b/.github/workflows/ci-clang-tidy.yml
index 015d42296a..ac4d01701e 100644
--- a/.github/workflows/ci-clang-tidy.yml
+++ b/.github/workflows/ci-clang-tidy.yml
@@ -34,33 +34,33 @@ on:
paths-ignore:
- "docs/**"
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
+ generate-matrix:
+ name: "Generate build matrix for ${{ github.workflow }}"
+ outputs:
+ matrix: ${{ steps.set-matrix.outputs.matrix }}
+ runs-on: ubuntu-24.04
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: 3.x
+ - id: set-matrix
+ run: python .github/generate-job-matrix.py --preset clang-tidy --seed 42 --debug combinations counts
build:
- name: "${{ matrix.formatting }} ${{ matrix.contracts }} C++${{ matrix.std }} ${{ matrix.config.name }} ${{ matrix.build_type }}"
+ name: "${{ matrix.config.name }} C++${{ matrix.std }} ${{ matrix.formatting }} ${{ matrix.contracts }} ${{ matrix.build_type }}"
runs-on: ${{ matrix.config.os }}
+ needs: generate-matrix
strategy:
fail-fast: false
matrix:
- formatting: ["std::format", "fmtlib"]
- contracts: ["none", "gsl-lite", "ms-gsl"]
- std: [20, 23]
- config:
- - {
- name: "Clang-18",
- os: ubuntu-24.04,
- compiler:
- {
- type: CLANG,
- version: 18,
- cc: "clang-18",
- cxx: "clang++-18",
- },
- lib: "libc++",
- cxx_modules: "False",
- std_format_support: "True",
- conan-config: "",
- }
- build_type: ["Release", "Debug"]
+ include: ${{fromJson(needs.generate-matrix.outputs.matrix)}}
env:
CC: ${{ matrix.config.compiler.cc }}
@@ -127,8 +127,11 @@ jobs:
sed -i.backup '/^\[settings\]$/,/^\[/ s/^compiler.cppstd=.*/compiler.cppstd=${{ matrix.std }}/' ~/.conan2/profiles/default
sed -i.backup '/^\[settings\]$/,/^\[/ s/^build_type=.*/build_type=${{ matrix.build_type }}/' ~/.conan2/profiles/default
conan profile show -pr default
- - run: echo "std_format=$([ "${{ matrix.formatting }}" == "std::format" ] && echo "True" || echo "False")" >> $GITHUB_ENV
- - run: echo "import_std=$([ "${{ matrix.config.cxx_modules }}" == "True" ] && [ "${{ matrix.config.contracts }}" == "none" ] && [ "${{ matrix.formatting }}" == "std::format" ] && echo "True" || echo "False")" >> $GITHUB_ENV
+ - name: Set 'std_format' and 'import_std' environment variables
+ shell: bash
+ run: |
+ echo "std_format=$([ "${{ matrix.formatting }}" == "std::format" ] && echo "True" || echo "False")" >> $GITHUB_ENV
+ echo "import_std=$([ "${{ matrix.std }}" -ge "23" ] && [ "${{ matrix.config.cxx_modules }}" == "True" ] && [ "${{ matrix.config.contracts }}" == "none" ] && [ "${{ matrix.formatting }}" == "std::format" ] && echo "True" || echo "False")" >> $GITHUB_ENV
- name: Run clang-tidy
shell: bash
run: |
diff --git a/.github/workflows/ci-conan.yml b/.github/workflows/ci-conan.yml
index 348ac4e1a1..186660aff9 100644
--- a/.github/workflows/ci-conan.yml
+++ b/.github/workflows/ci-conan.yml
@@ -24,162 +24,44 @@ name: Conan CI
on:
push:
- branches:
- - '**'
paths-ignore:
- "docs/**"
pull_request:
- branches:
- - '**'
paths-ignore:
- "docs/**"
env:
CHANNEL: ${{ fromJSON('["testing", "stable"]')[github.ref_type == 'tag' && startsWith(github.ref_name, 'v')] }}
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
+ generate-matrix:
+ name: "Generate build matrix for ${{ github.workflow }}"
+ outputs:
+ matrix: ${{ steps.set-matrix.outputs.matrix }}
+ runs-on: ubuntu-24.04
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: 3.x
+ - id: set-matrix
+ run: python .github/generate-job-matrix.py --preset conan --seed 42 --debug combinations counts
build:
- name: "${{ matrix.formatting }} ${{ matrix.contracts }} C++${{ matrix.std }} ${{ matrix.config.name }} ${{ matrix.build_type }}"
+ name: "${{ matrix.config.name }} C++${{ matrix.std }} ${{ matrix.formatting }} ${{ matrix.contracts }} ${{ matrix.build_type }}"
runs-on: ${{ matrix.config.os }}
+ needs: generate-matrix
strategy:
fail-fast: false
matrix:
- formatting: ["std::format", "fmtlib"]
- contracts: ["none", "gsl-lite", "ms-gsl"]
- std: [20, 23]
- config:
- - {
- name: "MSVC 14.4",
- os: windows-2022,
- compiler: { type: MSVC, version: 194, cc: "", cxx: "" },
- cxx_modules: "False",
- std_format_support: "True",
- conan-config: "",
- }
- - {
- name: "GCC-12",
- os: ubuntu-24.04,
- compiler:
- {
- type: GCC,
- version: 12,
- cc: "gcc-12",
- cxx: "g++-12",
- },
- cxx_modules: "False",
- std_format_support: "False",
- conan-config: "",
- }
- - {
- name: "GCC-13",
- os: ubuntu-24.04,
- compiler:
- {
- type: GCC,
- version: 13,
- cc: "gcc-13",
- cxx: "g++-13",
- },
- cxx_modules: "False",
- std_format_support: "True",
- conan-config: "",
- }
- - {
- name: "GCC-14",
- os: ubuntu-24.04,
- compiler:
- {
- type: GCC,
- version: 14,
- cc: "gcc-14",
- cxx: "g++-14",
- },
- cxx_modules: "False",
- std_format_support: "True",
- conan-config: "",
- }
- - {
- name: "Clang-16",
- os: ubuntu-22.04,
- compiler:
- {
- type: CLANG,
- version: 16,
- cc: "clang-16",
- cxx: "clang++-16",
- },
- lib: "libc++",
- cxx_modules: "False",
- std_format_support: "False",
- conan-config: "",
- }
- - {
- name: "Clang-17",
- os: ubuntu-24.04,
- compiler:
- {
- type: CLANG,
- version: 17,
- cc: "clang-17",
- cxx: "clang++-17",
- },
- lib: "libc++",
- cxx_modules: "True",
- std_format_support: "True",
- conan-config: "",
- }
- - {
- name: "Clang-18",
- os: ubuntu-24.04,
- compiler:
- {
- type: CLANG,
- version: 18,
- cc: "clang-18",
- cxx: "clang++-18",
- },
- lib: "libc++",
- cxx_modules: "True",
- std_format_support: "True",
- conan-config: "",
- }
- - {
- name: "Clang-18 on Apple M1 (arm64)",
- os: macos-14,
- compiler:
- {
- type: CLANG,
- version: 18,
- cc: "/opt/homebrew/opt/llvm@18/bin/clang-18",
- cxx: "/opt/homebrew/opt/llvm@18/bin/clang++",
- },
- lib: "libc++",
- cxx_modules: "False",
- std_format_support: "True"
- }
- - {
- name: "Apple Clang 15",
- os: macos-13,
- compiler:
- {
- type: APPLE_CLANG,
- version: "15.0",
- cc: "clang",
- cxx: "clang++",
- },
- cxx_modules: "False",
- std_format_support: "False",
- conan-config: "",
- }
- build_type: ["Release", "Debug"]
- exclude:
- - formatting: "std::format"
- config: { std_format_support: "False" }
-
+ include: ${{fromJson(needs.generate-matrix.outputs.matrix)}}
env:
CC: ${{ matrix.config.compiler.cc }}
CXX: ${{ matrix.config.compiler.cxx }}
-
steps:
- uses: actions/checkout@v4
- name: Generate unique cache id
@@ -262,21 +144,21 @@ jobs:
shell: bash
run: |
echo "std_format=$([ "${{ matrix.formatting }}" == "std::format" ] && echo "True" || echo "False")" >> $GITHUB_ENV
- echo "import_std=$([ "${{ matrix.config.cxx_modules }}" == "True" ] && [ "${{ matrix.config.contracts }}" == "none" ] && [ "${{ matrix.formatting }}" == "std::format" ] && echo "True" || echo "False")" >> $GITHUB_ENV
+ echo "import_std=$([ "${{ matrix.std }}" -ge "23" ] && [ "${{ matrix.config.cxx_modules }}" == "True" ] && [ "${{ matrix.config.contracts }}" == "none" ] && [ "${{ matrix.formatting }}" == "std::format" ] && echo "True" || echo "False")" >> $GITHUB_ENV
- name: Create Conan package
if: matrix.config.compiler.type != 'MSVC'
shell: bash
run: |
conan create . --user mpusz --channel ${CHANNEL} --lockfile-out=package.lock \
-b mp-units/* -b missing -c tools.cmake.cmaketoolchain:generator="Ninja Multi-Config" -c user.mp-units.build:all=True \
- -o '&:cxx_modules=${{ matrix.config.cxx_modules }}' -o '&:import_std=${{ env.import_std }}' -o '&:std_format=${{ env.std_format }}' -o '&:contracts=${{ matrix.contracts }}' ${{ matrix.config.conan-config }}
+ -o '&:cxx_modules=${{ matrix.config.cxx_modules }}' -o '&:import_std=${{ env.import_std }}' -o '&:std_format=${{ env.std_format }}' -o '&:contracts=${{ matrix.contracts }}' ${{ matrix.config.conan_config }}
- name: Create Conan package
if: matrix.config.compiler.type == 'MSVC'
shell: bash
run: |
conan create . --user mpusz --channel ${CHANNEL} --lockfile-out=package.lock \
-b mp-units/* -b missing -c tools.cmake.cmaketoolchain:generator="Ninja Multi-Config" -c user.mp-units.build:all=False \
- -o '&:cxx_modules=${{ matrix.config.cxx_modules }}' -o '&:import_std=${{ env.import_std }}' -o '&:std_format=${{ env.std_format }}' -o '&:contracts=${{ matrix.contracts }}' ${{ matrix.config.conan-config }}
+ -o '&:cxx_modules=${{ matrix.config.cxx_modules }}' -o '&:import_std=${{ env.import_std }}' -o '&:std_format=${{ env.std_format }}' -o '&:contracts=${{ matrix.contracts }}' ${{ matrix.config.conan_config }}
- name: Obtain package reference
id: get-package-ref
shell: bash
diff --git a/.github/workflows/ci-formatting.yml b/.github/workflows/ci-formatting.yml
index 086fe625a2..e61d5b8344 100644
--- a/.github/workflows/ci-formatting.yml
+++ b/.github/workflows/ci-formatting.yml
@@ -24,6 +24,10 @@ name: Formatting CI
on: [push, pull_request]
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
check:
runs-on: ubuntu-24.04
diff --git a/.github/workflows/ci-freestanding.yml b/.github/workflows/ci-freestanding.yml
index cde058db6d..ffdf5af547 100644
--- a/.github/workflows/ci-freestanding.yml
+++ b/.github/workflows/ci-freestanding.yml
@@ -34,51 +34,32 @@ on:
paths-ignore:
- "docs/**"
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
+ generate-matrix:
+ name: "Generate build matrix for ${{ github.workflow }}"
+ outputs:
+ matrix: ${{ steps.set-matrix.outputs.matrix }}
+ runs-on: ubuntu-24.04
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: 3.x
+ - id: set-matrix
+ run: python .github/generate-job-matrix.py --preset freestanding --seed 42 --debug combinations counts
build:
- name: "${{ matrix.formatting }} ${{ matrix.contracts }} C++${{ matrix.std }} ${{ matrix.config.name }} ${{ matrix.build_type }}"
+ name: "${{ matrix.config.name }} C++${{ matrix.std }} ${{ matrix.formatting }} ${{ matrix.contracts }} ${{ matrix.build_type }}"
runs-on: ${{ matrix.config.os }}
+ needs: generate-matrix
strategy:
fail-fast: false
matrix:
- formatting: ["std::format", "fmtlib"]
- contracts: ["none"]
- std: [23]
- config:
- - {
- name: "GCC-14",
- os: ubuntu-24.04,
- compiler:
- {
- type: GCC,
- version: 14,
- cc: "gcc-14",
- cxx: "g++-14",
- },
- cxx_modules: "False",
- std_format_support: "True",
- conan-config: "",
- }
- - {
- name: "Clang-18",
- os: ubuntu-24.04,
- compiler:
- {
- type: CLANG,
- version: 18,
- cc: "clang-18",
- cxx: "clang++-18",
- },
- lib: "libc++",
- cxx_modules: "True",
- std_format_support: "True",
- conan-config: "",
- }
- build_type: ["Release", "Debug"]
- # TODO For some reason Clang-18 Debug with -ffreestanding does not pass CMakeTestCXXCompiler
- exclude:
- - build_type: "Debug"
- config: { name: "Clang-18" }
+ include: ${{fromJson(needs.generate-matrix.outputs.matrix)}}
env:
CC: ${{ matrix.config.compiler.cc }}
diff --git a/.github/workflows/ci-test-package-cmake.yml b/.github/workflows/ci-test-package-cmake.yml
index d5bf5df0af..d58db7083d 100644
--- a/.github/workflows/ci-test-package-cmake.yml
+++ b/.github/workflows/ci-test-package-cmake.yml
@@ -38,136 +38,32 @@ on:
- "example/**"
- "test/**"
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
+ generate-matrix:
+ name: "Generate build matrix for ${{ github.workflow }}"
+ outputs:
+ matrix: ${{ steps.set-matrix.outputs.matrix }}
+ runs-on: ubuntu-24.04
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: 3.x
+ - id: set-matrix
+ run: python .github/generate-job-matrix.py --preset conan --seed 42 --debug combinations counts
test_package:
- name: "${{ matrix.formatting }} ${{ matrix.contracts }} C++${{ matrix.std }} ${{ matrix.config.name }} ${{ matrix.build_type }}"
+ name: "${{ matrix.config.name }} C++${{ matrix.std }} ${{ matrix.formatting }} ${{ matrix.contracts }} ${{ matrix.build_type }}"
runs-on: ${{ matrix.config.os }}
+ needs: generate-matrix
strategy:
fail-fast: false
matrix:
- formatting: ["std::format", "fmtlib"]
- contracts: ["none", "gsl-lite", "ms-gsl"]
- std: [20, 23]
- config:
- - {
- name: "MSVC 14.4",
- os: windows-2022,
- compiler: { type: MSVC, version: 194, cc: "", cxx: "" },
- cxx_modules: "False",
- std_format_support: "True",
- }
- - {
- name: "GCC-12",
- os: ubuntu-24.04,
- compiler:
- {
- type: GCC,
- version: 12,
- cc: "gcc-12",
- cxx: "g++-12",
- },
- cxx_modules: "False",
- std_format_support: "False",
- }
- - {
- name: "GCC-13",
- os: ubuntu-24.04,
- compiler:
- {
- type: GCC,
- version: 13,
- cc: "gcc-13",
- cxx: "g++-13",
- },
- cxx_modules: "False",
- std_format_support: "True",
- }
- - {
- name: "GCC-14",
- os: ubuntu-24.04,
- compiler:
- {
- type: GCC,
- version: 14,
- cc: "gcc-14",
- cxx: "g++-14",
- },
- cxx_modules: "False",
- std_format_support: "True"
- }
- - {
- name: "Clang-16",
- os: ubuntu-22.04,
- compiler:
- {
- type: CLANG,
- version: 16,
- cc: "clang-16",
- cxx: "clang++-16",
- },
- lib: "libc++",
- cxx_modules: "False",
- std_format_support: "False",
- }
- - {
- name: "Clang-17",
- os: ubuntu-24.04,
- compiler:
- {
- type: CLANG,
- version: 17,
- cc: "clang-17",
- cxx: "clang++-17",
- },
- lib: "libc++",
- cxx_modules: "False",
- std_format_support: "True",
- }
- - {
- name: "Clang-18",
- os: ubuntu-24.04,
- compiler:
- {
- type: CLANG,
- version: 18,
- cc: "clang-18",
- cxx: "clang++-18",
- },
- lib: "libc++",
- cxx_modules: "False",
- std_format_support: "True"
- }
- - {
- name: "Clang-18 on Apple M1 (arm64)",
- os: macos-14,
- compiler:
- {
- type: CLANG,
- version: 18,
- cc: "/opt/homebrew/opt/llvm@18/bin/clang-18",
- cxx: "/opt/homebrew/opt/llvm@18/bin/clang++",
- },
- lib: "libc++",
- cxx_modules: "False",
- std_format_support: "True"
- }
- - {
- name: "Apple Clang 15",
- os: macos-14,
- compiler:
- {
- type: APPLE_CLANG,
- version: "15.0",
- cc: "clang",
- cxx: "clang++",
- },
- cxx_modules: "False",
- std_format_support: "False",
- }
- build_type: ["Release", "Debug"]
- exclude:
- - formatting: "std::format"
- config: { std_format_support: "False" }
+ include: ${{fromJson(needs.generate-matrix.outputs.matrix)}}
env:
CC: ${{ matrix.config.compiler.cc }}
@@ -255,8 +151,8 @@ jobs:
- name: Set 'std_format' and 'import_std' environment variables
shell: bash
run: |
- echo "std_format=$([ "${{ matrix.formatting }}" == "std::format" ] && echo "True" || echo "False")" >> $GITHUB_ENV
- echo "import_std=$([ "${{ matrix.config.cxx_modules }}" == "True" ] && [ "${{ matrix.config.contracts }}" == "none" ] && [ "${{ matrix.formatting }}" == "std::format" ] && echo "True" || echo "False")" >> $GITHUB_ENV
+ echo "std_format=$([ "${{ matrix.formatting }}" == "std::format" ] && echo "True" || echo "False")" >> $GITHUB_ENV
+ echo "import_std=$([ "${{ matrix.std }}" -ge "23" ] && [ "${{ matrix.config.cxx_modules }}" == "True" ] && [ "${{ matrix.config.contracts }}" == "none" ] && [ "${{ matrix.formatting }}" == "std::format" ] && echo "True" || echo "False")" >> $GITHUB_ENV
- name: Install Conan dependencies
shell: bash
run: |
diff --git a/.github/workflows/citation.yml b/.github/workflows/citation.yml
index e5f22acf74..de8eb51c28 100644
--- a/.github/workflows/citation.yml
+++ b/.github/workflows/citation.yml
@@ -5,6 +5,10 @@ on:
paths:
- CITATION.cff
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
Validate-CITATION-cff:
runs-on: ubuntu-latest
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index c7e0abb58e..e742e8a174 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -22,6 +22,10 @@ on:
paths-ignore:
- "docs/**"
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
analyze:
name: Analyze
diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml
index 3072cd7967..09750e9ddd 100644
--- a/.github/workflows/documentation.yml
+++ b/.github/workflows/documentation.yml
@@ -36,6 +36,11 @@ on:
- "mkdocs.yml"
permissions:
contents: write
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
deploy:
runs-on: ubuntu-latest
diff --git a/conanfile.py b/conanfile.py
index c340711379..7b9c1853b5 100644
--- a/conanfile.py
+++ b/conanfile.py
@@ -58,7 +58,6 @@ class MPUnitsConan(ConanFile):
"cxx_modules": [True, False],
"import_std": [True, False],
"std_format": [True, False],
- "string_view_ret": [True, False],
"no_crtp": [True, False],
"contracts": ["none", "gsl-lite", "ms-gsl"],
"freestanding": [True, False],
@@ -67,7 +66,6 @@ class MPUnitsConan(ConanFile):
# "cxx_modules" default set in config_options()
# "import_std" default set in config_options()
# "std_format" default set in config_options()
- # "string_view_ret" default set in config_options()
# "no_crtp" default set in config_options()
"contracts": "gsl-lite",
"freestanding": False,
@@ -113,10 +111,6 @@ def _feature_compatibility(self):
"min_cppstd": "23",
"compiler": {"gcc": "", "clang": "18", "apple-clang": "", "msvc": ""},
},
- "static_constexpr_vars_in_constexpr_func": {
- "min_cppstd": "23",
- "compiler": {"gcc": "13", "clang": "17", "apple-clang": "", "msvc": ""},
- },
"explicit_this": {
"min_cppstd": "23",
"compiler": {
@@ -134,7 +128,6 @@ def _option_feature_map(self):
"std_format": "std_format",
"cxx_modules": "cxx_modules",
"import_std": "import_std",
- "string_view_ret": "static_constexpr_vars_in_constexpr_func",
"no_crtp": "explicit_this",
}
@@ -280,7 +273,6 @@ def generate(self):
tc.cache_variables["MP_UNITS_API_FREESTANDING"] = True
else:
tc.cache_variables["MP_UNITS_API_STD_FORMAT"] = opt.std_format
- tc.cache_variables["MP_UNITS_API_STRING_VIEW_RET"] = opt.string_view_ret
tc.cache_variables["MP_UNITS_API_NO_CRTP"] = opt.no_crtp
tc.cache_variables["MP_UNITS_API_CONTRACTS"] = str(opt.contracts).upper()
@@ -340,10 +332,6 @@ def package_info(self):
)
# handle API options
- self.cpp_info.components["core"].defines.append(
- "MP_UNITS_API_STRING_VIEW_RET="
- + str(int(self.options.string_view_ret == True))
- )
self.cpp_info.components["core"].defines.append(
"MP_UNITS_API_NO_CRTP=" + str(int(self.options.no_crtp == True))
)
diff --git a/docs/blog/posts/2.4.0-released.md b/docs/blog/posts/2.4.0-released.md
index 714379b48c..2e33210c58 100644
--- a/docs/blog/posts/2.4.0-released.md
+++ b/docs/blog/posts/2.4.0-released.md
@@ -1,5 +1,4 @@
---
-draft: true
date: 2024-11-05
authors:
- mpusz
diff --git a/docs/blog/posts/isq-part-1-introduction.md b/docs/blog/posts/isq-part-1-introduction.md
index e567e9e4ad..d49d399b3a 100644
--- a/docs/blog/posts/isq-part-1-introduction.md
+++ b/docs/blog/posts/isq-part-1-introduction.md
@@ -26,6 +26,7 @@ In this series, we will describe:
- [Part 3 - Modeling ISQ](isq-part-3-modeling-isq.md)
- [Part 4 - Implementing ISQ](isq-part-4-implemeting-isq.md)
- [Part 5 - Benefits](isq-part-5-benefits.md)
+- [Part 6 - Challenges](isq-part-6-challenges.md)
## Terms and Definitions
diff --git a/docs/blog/posts/isq-part-2-problems-when-isq-is-not-used.md b/docs/blog/posts/isq-part-2-problems-when-isq-is-not-used.md
index f64b094023..911d8a0510 100644
--- a/docs/blog/posts/isq-part-2-problems-when-isq-is-not-used.md
+++ b/docs/blog/posts/isq-part-2-problems-when-isq-is-not-used.md
@@ -28,6 +28,7 @@ library on just units or dimensions.
- [Part 3 - Modeling ISQ](isq-part-3-modeling-isq.md)
- [Part 4 - Implementing ISQ](isq-part-4-implemeting-isq.md)
- [Part 5 - Benefits](isq-part-5-benefits.md)
+- [Part 6 - Challenges](isq-part-6-challenges.md)
## Limitations of units-only solutions
diff --git a/docs/blog/posts/isq-part-3-modeling-isq.md b/docs/blog/posts/isq-part-3-modeling-isq.md
index a2ed08641b..fab9661ebb 100644
--- a/docs/blog/posts/isq-part-3-modeling-isq.md
+++ b/docs/blog/posts/isq-part-3-modeling-isq.md
@@ -27,6 +27,7 @@ language.
- Part 3 - Modeling ISQ
- [Part 4 - Implementing ISQ](isq-part-4-implemeting-isq.md)
- [Part 5 - Benefits](isq-part-5-benefits.md)
+- [Part 6 - Challenges](isq-part-6-challenges.md)
## Dimension is not enough to describe a quantity
@@ -150,8 +151,8 @@ flowchart TD
path_length --- distance["distance"]
distance --- radial_distance["radial_distance"]
length --- wavelength["wavelength"]
- length --- position_vector["position_vector
{vector}"]
length --- displacement["displacement
{vector}"]
+ displacement --- position_vector["position_vector"]
radius --- radius_of_curvature["radius_of_curvature"]
```
diff --git a/docs/blog/posts/isq-part-4-implemeting-isq.md b/docs/blog/posts/isq-part-4-implemeting-isq.md
index aff8f2858c..0eea0078d9 100644
--- a/docs/blog/posts/isq-part-4-implemeting-isq.md
+++ b/docs/blog/posts/isq-part-4-implemeting-isq.md
@@ -28,6 +28,7 @@ Now, it is time to see how we can implement hierarchies of quantities of the sam
- [Part 3 - Modeling ISQ](isq-part-3-modeling-isq.md)
- Part 4 - Implementing ISQ
- [Part 5 - Benefits](isq-part-5-benefits.md)
+- [Part 6 - Challenges](isq-part-6-challenges.md)
## Modeling a hierarchy of kind _length_
@@ -49,8 +50,8 @@ flowchart TD
path_length --- distance["distance"]
distance --- radial_distance["radial_distance"]
length --- wavelength["wavelength"]
- length --- position_vector["position_vector
{vector}"]
length --- displacement["displacement
{vector}"]
+ displacement --- position_vector["position_vector"]
radius --- radius_of_curvature["radius_of_curvature"]
```
@@ -74,8 +75,8 @@ inline constexpr auto arc_length = path_length;
inline constexpr struct distance final : quantity_spec {} distance;
inline constexpr struct radial_distance final : quantity_spec {} radial_distance;
inline constexpr struct wavelength final : quantity_spec {} wavelength;
-inline constexpr struct position_vector final : quantity_spec {} position_vector;
inline constexpr struct displacement final : quantity_spec {} displacement;
+inline constexpr struct position_vector final : quantity_spec {} position_vector;
```
Thanks to the expressivity and power of C++ templates, we can specify all quantity properties
diff --git a/docs/blog/posts/isq-part-5-benefits.md b/docs/blog/posts/isq-part-5-benefits.md
index c9d6343501..136e1e1f4d 100644
--- a/docs/blog/posts/isq-part-5-benefits.md
+++ b/docs/blog/posts/isq-part-5-benefits.md
@@ -26,6 +26,7 @@ how our ISQ model elegantly addresses the remaining problems.
- [Part 3 - Modeling ISQ](isq-part-3-modeling-isq.md)
- [Part 4 - Implementing ISQ](isq-part-4-implemeting-isq.md)
- Part 5 - Benefits
+- [Part 6 - Challenges](isq-part-6-challenges.md)
## Generic but safe interfaces
diff --git a/docs/blog/posts/isq-part-6-challenges.md b/docs/blog/posts/isq-part-6-challenges.md
new file mode 100644
index 0000000000..f658813e3d
--- /dev/null
+++ b/docs/blog/posts/isq-part-6-challenges.md
@@ -0,0 +1,530 @@
+---
+date: 2024-11-11
+authors:
+ - mpusz
+categories:
+ - Metrology
+comments: true
+---
+
+# International System of Quantities (ISQ): Part 6 - Challenges
+
+This article might be the last one from our series. This time, we will discuss the challenges and
+issues with modeling of the ISQ in software.
+
+
+
+## Articles from this series
+
+- [Part 1 - Introduction](isq-part-1-introduction.md)
+- [Part 2 - Problems when ISQ is not used](isq-part-2-problems-when-isq-is-not-used.md)
+- [Part 3 - Modeling ISQ](isq-part-3-modeling-isq.md)
+- [Part 4 - Implementing ISQ](isq-part-4-implemeting-isq.md)
+- [Part 5 - Benefits](isq-part-5-benefits.md)
+- Part 6 - Challenges
+
+
+## Ambiguity
+
+Some quantity names are ambiguous. It is not a problem of ISQ but of the English language and
+the way we communicate things. When I say: "Every _width_ is a _length_, but not every _length_
+is a _width_" most people understand this right away. However, the same people trying to model
+[our 3D box problem](isq-part-2-problems-when-isq-is-not-used.md#various-quantities-of-the-same-dimension-and-kinds)
+try to do it as follows:
+
+```cpp
+class Box {
+ quantity length_;
+ quantity width_;
+ quantity height_;
+public:
+ // ...
+};
+```
+
+This looks correct at first sight. Only when we think about the sentence mentioned above will
+we realize that this implementation has a problem. We intended to specify three orthogonal
+dimensions of the box, each of which will be a strong quantity that is not convertible to others.
+But we've failed.
+
+When we look at the
+[tree of quantities of length](isq-part-4-implemeting-isq.md#modeling-a-hierarchy-of-kind-length)
+we immediately see that both _width_ and _height_ are special _lengths_ so they are convertible to
+it.
+
+To implement our task correctly, we had to define and use a new quantity of kind _length_:
+
+```cpp
+inline constexpr struct horizontal_length final : quantity_spec {} horizontal_length;
+```
+
+We do not propose adding _horizontal length_ to ISO 80000-3. There are probably other similar
+cases as well, but so far, this was the most common and obvious one we've encountered.
+
+
+## No common quantities
+
+ISO 80000-1:2009 explicitly states:
+
+!!! quote
+
+ Two or more quantities cannot be added or subtracted unless they belong to the same category
+ of mutually comparable quantities.
+
+This means that we should be able to add and subtract any quantities as long as they belong to
+the same kind. However, ISO/IEC documents do not provide any rules or even hints about what should
+be the result of such operations.
+
+If it is possible to add _radius_ and _distance_, then what quantity should be provided
+in return? Undoubtedly, the resulting quantity type can't be the same as any of the arguments.
+It is not a _radius_ or _distance_. It is some closer unspecified _length_, though.
+
+!!! info
+
+ Finding the correct solution took us many months of experimentation and implementation.
+ Based on the hierarchy tree of quantities, we can define
+ [conversion rules](isq-part-3-modeling-isq.md#converting-between-quantities-of-the-same-kind)
+ and what a [common quantity should be](isq-part-3-modeling-isq.md#comparing-adding-and-subtracting-quantities-of-the-same-kind).
+
+
+## Lack of consistency
+
+The documents of ISO/IEC 80000 are not 100% consistent, and programming languages do not like
+inconsistencies.
+
+For example:
+
+- _time_ is mentioned as a base quantity of ISQ in ISO 80000-1 chapter 4.5.
+- ISO 80000-3 "Space and time", does not define a quantity of _time_. It provides a _duration_
+ quantity (item 3-9) with symbol _t_, and states in the Remarks section:
+
+ !!! quote
+
+ Duration is often just called time.
+
+- Other parts (e.g., IEC 80000-6 "Electromagnetism") often say:
+
+ !!! quote
+
+ ... _t_ is time (ISO 80000-3)
+
+To be consistent, ISO/IEC should either:
+
+- change ISO 80000-1 chapter 4.5 and all references in other parts to use _duration_ (unlikely),
+- or add _time_ as an alias name to _duration_ in the definition 3-9 of ISO 80000-3.
+
+
+## Lack of definitions
+
+ISQ defines derived quantities in terms of other quantities provided in the series. However, some
+definitions mention quantities that are not defined in the ISQ at all.
+
+For example, _weight_ is defined as $F_\textsf{g} = m\;g$, where $m$ is the _mass_ of the body
+(item 4-1 of ISO 80000-4 "Mechanics"), and $g$ is the _local acceleration of free fall_ (ISO 80000-3).
+
+The problem here is that ISO 80000-3 never defines a quantity with a symbol $g$ or named as a
+_local acceleration of free fall_. The closest one we have is _acceleration_ (item 3-11) with
+a symbol $a$.
+
+!!! info
+
+ To have a proper definition of _weight_ in **mp-units** that is not defined in terms of just
+ any kind of _acceleration_, we have added `isq::acceleration_of_free_fall` in our definitions
+ as an extension to the original ISQ set of quantities.
+
+
+## Not engineering-friendly
+
+Many quantities have proper physical definitions, but they are sometimes not engineering-friendly.
+
+For example, _velocity_ is defined as a rate of change of _position vector_
+$v = \frac{\textsf{d}r}{\textsf{d}t}$, where $r$ denotes the _position vector_ (item 3‑1.10) and
+$t$ the _duration_ (item 3‑9).
+
+Next, a _speed_ quantity is defined as the magnitude of _velocity_. Despite being
+physically correct, requiring every _speed_ to be derived from the vector quantity of _velocity_
+in software would be inconvenient. If this was the only case, people would always need to use
+vector representations of _position vectors_ to talk about _speeds_, which differs from what we
+do in practice. In practice, we divide any kind of _length_ by _time_ to get some kind of _speed_.
+
+ISO 80000-3 provides _length_, _height_, _distance_ and other quantities of kind _length_ that when
+divided by _duration_ can serve really well to calculate _speed_.
+
+!!! info
+
+ This is why in **mp-units**, we decided to divert from the official definition of _speed_ and
+ define it as:
+
+ ```cpp
+ inline constexpr struct speed : quantity_spec {} speed;
+ ```
+
+ This allows us to create a quantity of kind _speed_ from any quantity of _length_ divided
+ by _time_.
+
+ Additionally, it is essential to note that for the needs of our library, defining _velocity_
+ as `position_vector / duration` would be wrong. We miss the delta part here. Even though it is
+ not mentioned in ISO 80000-3, the delta of _position vectors_ is actually a _displacement_.
+ This is why our _velocity_ is defined as:
+
+ ```cpp
+ inline constexpr struct velocity : quantity_spec {} velocity;
+ ```
+
+ Please also note that _velocity_ is defined as a more specialized quantity of _speed_.
+
+
+## Affine space agnostic
+
+[The affine space](../../users_guide/framework_basics/the_affine_space.md) is a powerful
+abstraction, allowing us to model some problems safer or more accurately. It has two types of
+entities:
+
+- point - a position specified with coordinate values (e.g., location, address, etc.),
+- displacement vector - the difference between two points (e.g., shift, offset, displacement,
+ duration, etc.).
+
+Vectors support all the arithmetics operations, but points have some limitations. It is not
+possible to:
+
+- add two _points_,
+- subtract a _point_ from a _vector_,
+- multiply nor divide _points_ with anything else.
+
+ISO/IEC series does not acknowledge this abstraction even though it would be really useful in
+some cases. Let's discuss the following two examples.
+
+What does it mean to add two _altitudes_? It is not meaningful. On the other hand, subtracting
+those should not result in an _altitude_, but in a quantity of _height_. Adding or
+subtracting _height_ to/from _altitude_ results in _altitude_. Subtracting _altitude_ from
+_height_ is meaningless again. Those quantities clearly model affine space. Maybe this is why
+ISQ defines them as one quantity type _height_/_depth_/_altitude_?
+
+What does it mean to add two _position vectors_? It is not meaningful again. However, subtracting
+those results in a _displacement_ as we noted in the previous chapter. Adding or subtracting
+_displacement_ to/from _position vector_ results in another _position vector_, and subtracting
+_position vector_ from _displacement_ does not have physical sense. Again, those quantities
+perfectly model affine space. However, this time, those are defined as separate and independent
+quantities (i.e., displacement is not modeled as delta _position vector_ or _position vector_
+is not modeled as a _displacement_ from the origin of a coordinate system).
+
+!!! info
+
+ Currently, **mp-units** does not enforce the affine space behavior for such quantities.
+ Today, subtracting two _altitudes_ result in an _altitude_ and subtracting two
+ _position vectors_ result in a _position vector_. However, we plan to support automatic
+ conversion to a proper quantity type on subtraction and addition shortly.
+
+
+## Non-negative quantities
+
+Some quantities in the ISQ are defined as non-negative. This is a really interesting property that
+may be checked at runtime to increase safety. However, the number of such quantities is minimal.
+From a few hundred quantities provided by the ISO/IEC series, only the following have this property
+mentioned explicitly:
+
+- _width_/_breadth_,
+- _thickness_,
+- _diameter_,
+- _radius_.
+
+If _height_ was defined separately from _altitude_, it could probably also join this group.
+
+Let's think a bit more about this. What does it mean that a quantity is non-negative? Indeed,
+it is hard to imagine something of a negative _width_ or _radius_. However, if we subtract
+two _widths_, the second one may be larger. This will result in a negative
+quantity of _width_, violating our precondition. So, is it non-negative or not?
+
+Again, we have to talk about the affine space abstractions. Every empirical measurement can be
+expressed as a point. Such points for some quantities may be non-negative indeed.
+
+Non-negative quantities do not end on the ones provided above. For example, _speed_ is a good
+example here as well. In general, all magnitudes of vector quantities will also have this property.
+
+When subtracting two points, we end up with a delta/displacement type, which may be negative
+even for quantities listed as non-negative in the ISQ. As stated in the previous chapter,
+having affine space abstractions acknowledged in ISQ would greatly help here.
+
+
+## Lack of quantity recipes
+
+Definition of many derived quantities provides their recipes in the form of
+[quantity equations](../../appendix/glossary.md#quantity-equation) (e.g., _weight_ equation
+in the previous chapter). However, some of them do not. Instead, they often provide a very
+generic description.
+
+For example, _force_ is defined as:
+
+!!! quote
+
+ vector (ISO 80000-2) quantity describing interaction between bodies or particles.
+
+This is not helpful for programming languages that like explicit definitions. Different
+vendors may interpret the above differently, which will result in different implementations that
+will not be compatible with each other.
+
+As the derived quantity of _force_ has to be a vector quantity, it has to be defined in terms of
+at least one other vector quantity. We have a few to choose from:
+
+- _displacement_ ($\Delta{r}$),
+- _velocity_ ($v$),
+- _acceleration_ ($a$).
+
+It is not stated explicitly in ISQ which one of those should be used and how.
+
+!!! info
+
+ In **mp-units** we decided to define _force_ as $F = m\;a$.
+
+
+## Lack of generic quantities and name conflicts
+
+In the previous chapter, we complained about some definitions needing to be more complex or generic.
+On the other hand, we also lack some generic quantities in ISQ that could serve as a root for
+a quantity hierarchy tree.
+
+For example:
+
+- ISO 80000-4 "Mechanics" defines _power <mechanics>_ as $P = F\;v$ (scalar product of force $F$
+ (item 4-9.1) acting to a body and its velocity $v$ (ISO 80000-3)),
+- ISO 80000-6 "Electromagnetism" defines _power_ as $p = u\;i$ (scalar quantity given by the
+ product of _instantaneous voltage_ $u$ (item 6-11.3) and _instantaneous electric current_ $i$
+ (item 6-1)).
+
+First, the above definitions have somehow conflicting names which makes it hard for the programming
+languages to name them consistently by different vendors.
+
+!!! info
+
+ In **mp-units**, we chose `mechanical_power` and `electromagnetism_power` for those.
+
+Second, we do not have any other more generic definition of _power_ to put above those in the tree.
+Not having it makes it hard to answer what should be the result of:
+
+```cpp
+quantity q = isq::mechanical_power(42 * W) + isq::electromagnetism_power(60 * W);
+```
+
+!!! info
+
+ To solve the above problem, we have added `isq::power` in **mp-units**, that has a really
+ generic definition of:
+
+ ```cpp
+ inline constexpr struct power : quantity_spec(length) / pow<3>(time)> {} power;
+ ```
+
+
+## Invalid definitions order
+
+_Energy_ is defined a bit better than _power_, but still not without issues.
+
+The first time ISQ mentions _energy_ is in the ISO 80000-4 "Mechanics". It defines
+_potential energy_, _kinetic energy_, and a _mechanical energy_ as the sum of the first two.
+Right after that a _mechanical work/work_ is defined.
+
+Then ISO 80000-5 "Thermodynamics" defines _energy <thermodynamics>_ as:
+
+!!! quote
+
+ ability of a system to do work (ISO 80000-4).
+
+Next, _internal energy/thermodynamic energy_ is defined in terms of the change of heat.
+
+From the above, it seems that what is called _energy <thermodynamics>_ should actually be
+the root of our tree and probably be provided in Part 4 before the _mechanical energy_ is defined.
+
+
+## Hierarchies of derived quantities
+
+Derived quantities of the same kind are often independently defined in the ISQ. The ISO/IEC 80000
+series often does not suggest any hierarchy between those. Even more, it states:
+
+!!! quote "ISO/IEC Guide 99"
+
+ The division of ‘quantity’ according to ‘kind of quantity’ is, to some extent, arbitrary.
+
+Because of this, it is unknown or ambiguous how to form a hierarchy tree for such quantities.
+
+To get some sense of the complexity here, let's look again at our tree of quantities of a kind
+_energy_:
+
+```mermaid
+flowchart TD
+ energy["energy
(mass * length2 / time2)
[J]"]
+ energy --- mechanical_energy["mechanical_energy"]
+ mechanical_energy --- potential_energy["potential_energy"]
+ mechanical_energy --- kinetic_energy["kinetic_energy"]
+ energy --- enthalpy["enthalpy"]
+ enthalpy --- internal_energy["internal_energy / thermodynamic_energy"]
+ internal_energy --- Helmholtz_energy["Helmholtz_energy / Helmholtz_function"]
+ enthalpy --- Gibbs_energy["Gibbs_energy / Gibbs_function"]
+ energy --- active_energy["active_energy"]
+```
+
+Not being exact means that every vendor may implement it differently. This will result in:
+
+- different convertibility rules among quantities:
+
+ ```cpp
+ static_assert(implicitly_convertible(isq::potential_energy, isq::mechanical_energy));
+ static_assert(explicitly_convertible(isq::mechanical_energy, isq::potential_energy));
+ ```
+
+- different common quantities resulting from the arithmetics on various quantities of the same
+ kind:
+
+ ```cpp
+ static_assert((isq::potential_energy(1 * J) + isq::kinetic_energy(1 * J)).quantity_spec == isq::mechanical_energy);
+ ```
+
+It would be great if ISQ could provide specific division of quantities into kinds and more
+information about the position of each quantity within the hierarchy of quantities of
+the same kind.
+
+!!! important
+
+ We can try to do this by ourselves, but it is tough. Probably no one, for sure we are
+ not, is an expert in all the fields of ISO/IEC 80000 applicability.
+
+ We need the help of subject matter experts who will help us build those trees for their domains
+ and then verify that everything works as expected.
+
+
+## The same or a different kind?
+
+Some quantities are more complicated than others. For example, _power_ has:
+
+- scalar quantities expressed in:
+ - W (watts) (e.g., _mechanical power_, _active power_),
+ - VA (volt-ampere) (e.g., _apparent power_),
+ - var (e.g., _reactive power_),
+- complex quantities expressed in VA (volt-ampere) (e.g., _complex power_).
+
+How should we model this? Maybe those should be two or three independent trees of quantities, each
+having its own unit?
+
+```mermaid
+flowchart TD
+ power["power
(mass * length2 / time3)
[W]"]
+ power --- mechanical_power["mechanical_power
(scalar_product(force, velocity))"]
+ power --- electromagnetism_power["electromagnetism_power | instantaneous_power
(instantaneous_voltage * instantaneous_electric_current)"]
+ power --- active_power["active_power
(1 / period * instantaneous_power * time)
(re(complex_power))"]
+
+ nonactive_power["nonactive_power
(mass * length2 / time3)
[VA]"]
+ nonactive_power --- reactive_power["reactive_power
(im(complex_power))
[var]"]
+
+ complex_power["complex_power
{complex}
(voltage_phasor * electric_current_phasor)
(active_power + j * reactive_power)
[VA]"]
+ complex_power --- apparent_power["apparent_power
(voltage * electric_current)
(mod(complex_power))"]
+```
+
+This will mean that we will not be able to add or compare _active power_, _reactive power_, and
+_apparent power_, which probably makes a lot of sense. However, it also means that the following
+will fail to compile:
+
+```cpp
+quantity apparent = isq::apparent_power(100 * VA);
+quantity active = isq::active_power(60 * W);
+quantity q = sqrt(pow<2>(apparent) - pow<2>(active)); // Compile-time error
+```
+
+Also the following will not work:
+
+```cpp
+quantity active = isq::active_power(60 * W);
+quantity reactive = isq::reactive_power(40 * var);
+quantity q = sqrt(pow<2>(active) + pow<2>(reactive)); // Compile-time error
+```
+
+If we want the above to work maybe we need to implement the tree as follows?
+
+```mermaid
+flowchart TD
+ power["power
(mass * length2 / time3)
[W]"]
+ power --- mechanical_power["mechanical_power
(scalar_product(force, velocity))"]
+ power --- electromagnetism_power["electromagnetism_power | instantaneous_power
(instantaneous_voltage * instantaneous_electric_current)"]
+ power --- apparent_power["apparent_power
(voltage * electric_current)
(mod(complex_power))
[VA]"]
+ apparent_power --- active_power["active_power
(1 / period * instantaneous_power * time)
(re(complex_power))"]
+ apparent_power --- nonactive_power["nonactive_power
(sqrt(apparent_power2 - active_power2))
"]
+ nonactive_power --- reactive_power["reactive_power
(im(complex_power))
[var]"]
+ apparent_power --- complex_power["complex_power
{complex}
(voltage_phasor * electric_current_phasor)
(active_power + j * reactive_power)"]
+```
+
+However, the above allows direct addition and comparison of _active power_ and _nonactive power_,
+and also will not complain if someone will try to use watt (W) as a unit of _apparent power_ or
+_reactive power_.
+
+Again, ISQ does not provide a direct answer here.
+
+
+## More base quantities?
+
+Is ISQ really based on only seven base quantities? Let's look at the definition of
+_traffic intensity_ in IEC 80000-13 "Information science and technology":
+
+!!! quote
+
+ number of simultaneously busy resources in a particular pool of resources.
+
+It looks like a definition of a specialized dimensionless quantity or, more correctly, a quantity
+of dimension one. This would not be the only such case. Even in the same Part 13, we can find
+quantities like _storage capacity_ with a similar property.
+
+Only when we look closer do we start to see differences. All dimensionless quantities, even if they
+have their own dedicated units, can also be measured in a unit of one (1). This is true for
+_storage capacity_ (also measured in bits), _angular measure_ (also measured in radians),
+_solid angular measure (also measured in steradians), and more.
+
+However, _traffic intensity_ can only be measured in erlangs (E), not in a unit one (1).
+Does it mean that it is a "hidden" 8-th base quantity in ISQ? If so, should it have its own
+dimension as well?
+
+Angular quantities are another interesting case here. Scientists have written petitions and papers
+for years to make them an additional dimension in ISQ and SI. More about this can be found in
+our documentation's [Strong Angular System](../../users_guide/systems/strong_angular_system.md)
+chapter.
+
+
+## Summary
+
+ISQ is tremendous and solves many problems we had in modeling various subjects for years in
+software. As a result, we have more powerful tools in our hands that allow us to deliver safer
+products.
+
+Unfortunately, ISQ, contrarily to SI, is not widely recognized, and no libraries besides
+**mp-units** model it in any programming language. Keeping it behind a paywall does not help
+either. We hope that posts from this series will spread in the community, raise awareness
+of ISQ and its benefits, and encourage authors of other libraries to implement it in their
+products.
+
+Despite all the benefits, it is essential to realize that ISQ has many problems. International
+standards should be specified in such a way that there is no room for ambiguity in their
+interpretation by different parties trying to use them. As described above, this is not the case
+here.
+
+ISQ is not ready to be unambiguously modeled in software by various vendors. Here are the most
+important problems to solve to allow this:
+
+1. ISQ needs to define basic operations on quantities:
+
+ - what the result of addition and subtraction should be when arguments differ,
+ - convertibility rules.
+
+2. The exact quantity equation recipe needs to be included for many derived quantities.
+3. Many ISQ quantities do not provide their exact relation versus other quantities of the same
+ kind (no strict hierarchy).
+4. Some missing quantities need to be included. Others would benefit from corrected names.
+
+Additionally:
+
+- extending ISQ with affine space abstractions,
+- specifying more quantities as non-negative,
+- adding more base quantities (i.e., _angle_)
+
+could improve the safety of our programs and products that people depend on with their lives on
+a daily basis.
+
+I hope you enjoyed following this series and learned more about the International System
+of Quantities. Please try it out in your domain and share feedback with us. We always love to
+hear about the projects in which our library is being used and about use cases it helps
+address.
diff --git a/docs/getting_started/cpp_compiler_support.md b/docs/getting_started/cpp_compiler_support.md
index d5eb5a3480..525585c302 100644
--- a/docs/getting_started/cpp_compiler_support.md
+++ b/docs/getting_started/cpp_compiler_support.md
@@ -19,7 +19,6 @@ C++ feature:
| **`std::format`** | 20 | 13 | 17 | None | 194 |
| **C++ modules** | 20 | None | 17 | None | None |
| **`import std;`** | 23 | None | 18 | None | None |
-| **Static `constexpr` variables in `constexpr` functions** | 23 | 13 | 17 | None | None |
| **Explicit `this` parameter** | 23 | 14 | 18 | None | None |
??? note "MSVC bugs"
@@ -87,20 +86,6 @@ C++ feature:
- CMake: [CMAKE_CXX_MODULE_STD](https://cmake.org/cmake/help/latest/variable/CMAKE_CXX_MODULE_STD.html)
-## Static `constexpr` variables in `constexpr` functions
-
-- Allows returning `std::string_view` from the
- [`unit_symbol()`](../users_guide/framework_basics/text_output.md#unit_symbol)
- and [`dimension_symbol()`](../users_guide/framework_basics/text_output.md#dimension_symbol)
- functions
- - `std::string_view` type has a reference semantics so it has to point to a storage with
- a longer lifetime.
-- If this feature is not available, the API returns `mp_units::basic_fixed_string` instead.
-- Tested as `__cpp_constexpr >= 202211L` [feature test macro](https://en.cppreference.com/w/cpp/feature_test).
-- Related build options:
- - Conan: [string_view_ret](installation_and_usage.md#string_view_ret)
- - CMake: [MP_UNITS_API_STRING_VIEW_RET](installation_and_usage.md#MP_UNITS_API_STRING_VIEW_RET)
-
## Explicit `this` parameter
- This feature removes the need for the usage of the CRTP idiom in the
diff --git a/docs/getting_started/installation_and_usage.md b/docs/getting_started/installation_and_usage.md
index 79de4a58cc..8fb6115ba8 100644
--- a/docs/getting_started/installation_and_usage.md
+++ b/docs/getting_started/installation_and_usage.md
@@ -123,18 +123,6 @@ dependencies by other means, some modifications to the library's CMake files mig
[conan std::format support]: https://github.com/mpusz/mp-units/releases/tag/v2.2.0
-[`string_view_ret`](#string_view_ret){ #string_view_ret }
-
-: [:octicons-tag-24: 2.2.0][conan returning string_view] · :octicons-milestone-24: `True`/`False` (Default: automatically determined from settings)
-
- Enables returning `std::string_view` from the
- [`unit_symbol()`](../users_guide/framework_basics/text_output.md#unit_symbol)
- and [`dimension_symbol()`](../users_guide/framework_basics/text_output.md#dimension_symbol)
- functions. If this feature is not available, those functions will return
- `mp_units::basic_fixed_string` instead.
-
- [conan returning string_view]: https://github.com/mpusz/mp-units/releases/tag/v2.2.0
-
[`no_crtp`](#no_crtp){ #no_crtp }
: [:octicons-tag-24: 2.2.0][conan no crtp support] · :octicons-milestone-24: `True`/`False` (Default: automatically determined from settings)
@@ -196,18 +184,6 @@ dependencies by other means, some modifications to the library's CMake files mig
[cmake std::format support]: https://github.com/mpusz/mp-units/releases/tag/v2.2.0
- [`MP_UNITS_API_STRING_VIEW_RET`](#MP_UNITS_API_STRING_VIEW_RET){ #MP_UNITS_API_STRING_VIEW_RET }
-
- : [:octicons-tag-24: 2.2.0][cmake returning string_view] · :octicons-milestone-24: `ON`/`OFF` (Default: automatically determined)
-
- Enables returning `std::string_view` from the
- [`unit_symbol()`](../users_guide/framework_basics/text_output.md#unit_symbol)
- and [`dimension_symbol()`](../users_guide/framework_basics/text_output.md#dimension_symbol)
- functions. If this feature is not available, those functions will return
- `mp_units::basic_fixed_string` instead.
-
- [cmake returning string_view]: https://github.com/mpusz/mp-units/releases/tag/v2.2.0
-
[`MP_UNITS_API_NO_CRTP`](#MP_UNITS_API_NO_CRTP){ #MP_UNITS_API_NO_CRTP }
: [:octicons-tag-24: 2.2.0][cmake no crtp support] · :octicons-milestone-24: `ON`/`OFF` (Default: automatically determined)
diff --git a/docs/users_guide/examples/si_constants.md b/docs/users_guide/examples/si_constants.md
index f5feb15353..847520f4e3 100644
--- a/docs/users_guide/examples/si_constants.md
+++ b/docs/users_guide/examples/si_constants.md
@@ -16,19 +16,11 @@ work in practice.
--8<-- "example/si_constants.cpp:28:40"
```
-As always, we start with the inclusion of all the needed header files. After that, for
-the simplicity of this example, we
-[hack the character of quantities](../framework_basics/character_of_a_quantity.md#hacking-the-character)
-to be able to express vector quantities with simple scalar types.
-
-```cpp title="si_constants.cpp" linenums="14"
---8<-- "example/si_constants.cpp:42:44"
-```
-
+As always, we start with the inclusion of all the needed header files.
The main part of the example prints all of the SI-defining constants:
-```cpp title="si_constants.cpp" linenums="17"
---8<-- "example/si_constants.cpp:45:"
+```cpp title="si_constants.cpp" linenums="14"
+--8<-- "example/si_constants.cpp:42:"
```
While analyzing the output of this program (provided below), we can easily notice that a direct
diff --git a/docs/users_guide/framework_basics/character_of_a_quantity.md b/docs/users_guide/framework_basics/character_of_a_quantity.md
index 2ce5a16625..769fe55d81 100644
--- a/docs/users_guide/framework_basics/character_of_a_quantity.md
+++ b/docs/users_guide/framework_basics/character_of_a_quantity.md
@@ -97,22 +97,22 @@ enumeration can be appended to the `quantity_spec` describing such a quantity ty
=== "C++23"
```cpp
- inline constexpr struct position_vector final : quantity_spec {} position_vector;
inline constexpr struct displacement final : quantity_spec {} displacement;
+ inline constexpr struct position_vector final : quantity_spec {} position_vector;
```
=== "C++20"
```cpp
- inline constexpr struct position_vector final : quantity_spec {} position_vector;
inline constexpr struct displacement final : quantity_spec {} displacement;
+ inline constexpr struct position_vector final : quantity_spec {} position_vector;
```
=== "Portable"
```cpp
- QUANTITY_SPEC(position_vector, length, quantity_character::vector);
QUANTITY_SPEC(displacement, length, quantity_character::vector);
+ QUANTITY_SPEC(position_vector, displacement);
```
With the above, all the quantities derived from `position_vector` or `displacement` will have a correct
@@ -126,19 +126,19 @@ character override is needed):
=== "C++23"
```cpp
- inline constexpr struct velocity final : quantity_spec {} velocity;
+ inline constexpr struct velocity final : quantity_spec {} velocity;
```
=== "C++20"
```cpp
- inline constexpr struct velocity final : quantity_spec {} velocity;
+ inline constexpr struct velocity final : quantity_spec {} velocity;
```
=== "Portable"
```cpp
- QUANTITY_SPEC(velocity, speed, position_vector / duration);
+ QUANTITY_SPEC(velocity, speed, displacement / duration);
```
@@ -148,7 +148,7 @@ As we remember, the `quantity` class template is defined as follows:
```cpp
template Rep = double>
+ RepresentationOf Rep = double>
class quantity;
```
diff --git a/docs/users_guide/framework_basics/concepts.md b/docs/users_guide/framework_basics/concepts.md
index 797e7201a1..c8177d39d1 100644
--- a/docs/users_guide/framework_basics/concepts.md
+++ b/docs/users_guide/framework_basics/concepts.md
@@ -159,15 +159,28 @@ A `Reference` can either be:
[value of a quantity](../../appendix/glossary.md#quantity-value).
-### `RepresentationOf` { #RepresentationOf }
+### `RepresentationOf` { #RepresentationOf }
-`RepresentationOf` concept is satisfied by all `Representation` types that are of a specified
-[quantity character](../../appendix/glossary.md#character) `Ch`.
+`RepresentationOf` concept is satisfied:
+
+- if the type of `V` satisfies [`QuantitySpec`](#QuantitySpec):
+
+ - by all [`Representation`](#Representation) types when `V` describes
+ a [quantity kind](../../appendix/glossary.md#kind),
+ - otherwise, by [`Representation`](#Representation) types that are of
+ a [quantity character](../../appendix/glossary.md#character) associated with a provided
+ quantity specification `V`.
+
+- if `V` is of `quantity_character` type:
+
+ - by [`Representation`](#Representation) types that are of a provided
+ [quantity character](../../appendix/glossary.md#character).
A user can declare a custom representation type to be of a specific character by providing the specialization
with `true` for one or more of the following variable templates:
- `is_scalar`
+- `is_complex`
- `is_vector`
- `is_tensor`
diff --git a/docs/users_guide/framework_basics/design_overview.md b/docs/users_guide/framework_basics/design_overview.md
index 7446bea7fd..3e5de7037e 100644
--- a/docs/users_guide/framework_basics/design_overview.md
+++ b/docs/users_guide/framework_basics/design_overview.md
@@ -316,7 +316,7 @@ This is why a `quantity` class template is defined in the library as:
```cpp
template Rep = double>
+ RepresentationOf Rep = double>
class quantity;
```
@@ -365,7 +365,7 @@ In the **mp-units** library, the quantity point is implemented as:
```cpp
template auto PO,
- RepresentationOf Rep = double>
+ RepresentationOf Rep = double>
class quantity_point;
```
diff --git a/docs/users_guide/framework_basics/simple_and_typed_quantities.md b/docs/users_guide/framework_basics/simple_and_typed_quantities.md
index 178c36e24f..dd7ee228a5 100644
--- a/docs/users_guide/framework_basics/simple_and_typed_quantities.md
+++ b/docs/users_guide/framework_basics/simple_and_typed_quantities.md
@@ -20,7 +20,7 @@ In the **mp-units** library, a quantity is represented with the following class
```cpp
template Rep = double>
+ RepresentationOf Rep = double>
class quantity;
```
diff --git a/docs/users_guide/framework_basics/systems_of_quantities.md b/docs/users_guide/framework_basics/systems_of_quantities.md
index 3c24801a8b..f5df4d8204 100644
--- a/docs/users_guide/framework_basics/systems_of_quantities.md
+++ b/docs/users_guide/framework_basics/systems_of_quantities.md
@@ -109,8 +109,8 @@ flowchart TD
path_length --- distance["distance"]
distance --- radial_distance["radial_distance"]
length --- wavelength["wavelength"]
- length --- position_vector["position_vector
{vector}"]
length --- displacement["displacement
{vector}"]
+ displacement --- position_vector["position_vector"]
radius --- radius_of_curvature["radius_of_curvature"]
```
@@ -164,8 +164,8 @@ For example, here is how the above quantity kind tree can be modeled in the libr
inline constexpr struct distance final : quantity_spec {} distance;
inline constexpr struct radial_distance final : quantity_spec {} radial_distance;
inline constexpr struct wavelength final : quantity_spec {} wavelength;
- inline constexpr struct position_vector final : quantity_spec {} position_vector;
inline constexpr struct displacement final : quantity_spec {} displacement;
+ inline constexpr struct position_vector final : quantity_spec {} position_vector;
```
=== "C++20"
@@ -186,8 +186,8 @@ For example, here is how the above quantity kind tree can be modeled in the libr
inline constexpr struct distance final : quantity_spec {} distance;
inline constexpr struct radial_distance final : quantity_spec {} radial_distance;
inline constexpr struct wavelength final : quantity_spec {} wavelength;
- inline constexpr struct position_vector final : quantity_spec {} position_vector;
inline constexpr struct displacement final : quantity_spec {} displacement;
+ inline constexpr struct position_vector final : quantity_spec {} position_vector;
```
=== "Portable"
@@ -208,8 +208,8 @@ For example, here is how the above quantity kind tree can be modeled in the libr
QUANTITY_SPEC(distance, path_length);
QUANTITY_SPEC(radial_distance, distance);
QUANTITY_SPEC(wavelength, length);
- QUANTITY_SPEC(position_vector, length, quantity_character::vector);
QUANTITY_SPEC(displacement, length, quantity_character::vector);
+ QUANTITY_SPEC(position_vector, displacement);
```
!!! note
diff --git a/docs/users_guide/framework_basics/the_affine_space.md b/docs/users_guide/framework_basics/the_affine_space.md
index 688c4379e3..50a297722e 100644
--- a/docs/users_guide/framework_basics/the_affine_space.md
+++ b/docs/users_guide/framework_basics/the_affine_space.md
@@ -96,7 +96,7 @@ origin:
```cpp
template auto PO = default_point_origin(R),
- RepresentationOf Rep = double>
+ RepresentationOf Rep = double>
class quantity_point;
```
diff --git a/example/currency.cpp b/example/currency.cpp
index 43aa28fd6d..bedf7c05ea 100644
--- a/example/currency.cpp
+++ b/example/currency.cpp
@@ -62,21 +62,6 @@ inline constexpr auto JPY = japanese_jen;
static_assert(!std::equality_comparable_with, quantity>);
-
-#if 0 || !MP_UNITS_API_STRING_VIEW_RET // NOLINT(readability-avoid-unconditional-preprocessor-if)
-
-// if you have only a few currencies to handle
-template
-[[nodiscard]] double exchange_rate(std::chrono::sys_seconds timestamp)
-{
- (void)timestamp; // get conversion ratios for this timestamp
- if constexpr (From == us_dollar && To == euro) return 0.9215;
- else if constexpr (From == euro && To == us_dollar) return 1.0848;
- // ...
-}
-
-#else
-
template
[[nodiscard]] double exchange_rate(std::chrono::sys_seconds timestamp)
{
@@ -89,8 +74,6 @@ template
return rates.at(std::make_pair(unit_symbol(From), unit_symbol(To)));
}
-#endif
-
template auto To, QuantityOf From>
QuantityOf auto exchange_to(From q, std::chrono::sys_seconds timestamp)
{
diff --git a/example/include/validated_type.h b/example/include/validated_type.h
index c8b5dc6a71..70be6dac3d 100644
--- a/example/include/validated_type.h
+++ b/example/include/validated_type.h
@@ -129,8 +129,8 @@ std::basic_ostream& operator<<(std::basic_ostream&
template
struct MP_UNITS_STD_FMT::formatter, Char> : formatter {
template
- auto format(const validated_type& v, FormatContext& ctx) const -> decltype(ctx.out())
+ auto format(const validated_type& val, FormatContext& ctx) const -> decltype(ctx.out())
{
- return formatter::format(v.value(), ctx);
+ return formatter::format(val.value(), ctx);
}
};
diff --git a/example/kalman_filter/kalman_filter-example_2.cpp b/example/kalman_filter/kalman_filter-example_2.cpp
index b03f872483..5a07edebd5 100644
--- a/example/kalman_filter/kalman_filter-example_2.cpp
+++ b/example/kalman_filter/kalman_filter-example_2.cpp
@@ -61,7 +61,7 @@ void print(auto iteration, QuantityPoint auto measured, const kalman::SystemStat
int main()
{
using namespace mp_units::si::unit_symbols;
- using qp = quantity_point;
+ using qp = quantity_point;
using state = kalman::system_state>;
const quantity interval = isq::duration(5 * s);
diff --git a/example/kalman_filter/kalman_filter-example_3.cpp b/example/kalman_filter/kalman_filter-example_3.cpp
index 9c2ca7d4a9..65aa2b1f21 100644
--- a/example/kalman_filter/kalman_filter-example_3.cpp
+++ b/example/kalman_filter/kalman_filter-example_3.cpp
@@ -61,7 +61,7 @@ void print(auto iteration, QuantityPoint auto measured, const kalman::SystemStat
int main()
{
using namespace mp_units::si::unit_symbols;
- using qp = quantity_point;
+ using qp = quantity_point;
using state = kalman::system_state>;
const quantity interval = isq::duration(5 * s);
diff --git a/example/kalman_filter/kalman_filter-example_4.cpp b/example/kalman_filter/kalman_filter-example_4.cpp
index 20813ad829..64e240e1f5 100644
--- a/example/kalman_filter/kalman_filter-example_4.cpp
+++ b/example/kalman_filter/kalman_filter-example_4.cpp
@@ -62,7 +62,7 @@ void print(auto iteration, QuantityPoint auto measured, const kalman::SystemStat
int main()
{
using namespace mp_units::si::unit_symbols;
- using qp = quantity_point;
+ using qp = quantity_point;
using state =
kalman::system_state, quantity_point>;
diff --git a/example/si_constants.cpp b/example/si_constants.cpp
index e09588c4b1..a43779abae 100644
--- a/example/si_constants.cpp
+++ b/example/si_constants.cpp
@@ -39,10 +39,6 @@ import mp_units;
#include
#endif
-template
- requires mp_units::is_scalar
-constexpr bool mp_units::is_vector = true;
-
int main()
{
using namespace mp_units;
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 8b19f595b4..ca5b7ed1f2 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -48,7 +48,6 @@ endif()
# check for C++ features
check_cxx_feature_supported(__cpp_lib_format ${projectPrefix}LIB_FORMAT_SUPPORTED)
-check_cxx_feature_supported("__cpp_constexpr >= 202211L" ${projectPrefix}STATIC_CONSTEXPR_VARS_IN_CONSTEXPR_FUNCTIONS)
check_cxx_feature_supported(__cpp_explicit_this_parameter ${projectPrefix}EXPLICIT_THIS_PARAMETER_SUPPORTED)
# libc++ has a basic supports for std::format but does not set __cpp_lib_format
@@ -73,9 +72,6 @@ endif()
# project API settings
option(${projectPrefix}API_STD_FORMAT "Enable `std::format` support" ${${projectPrefix}LIB_FORMAT_SUPPORTED})
-option(${projectPrefix}API_STRING_VIEW_RET "Enable returning `std::string_view` from `constexpr` functions"
- ${${projectPrefix}STATIC_CONSTEXPR_VARS_IN_CONSTEXPR_FUNCTIONS}
-)
option(${projectPrefix}API_NO_CRTP "Enable class definitions without CRTP idiom"
${${projectPrefix}EXPLICIT_THIS_PARAMETER_SUPPORTED}
)
@@ -83,7 +79,6 @@ option(${projectPrefix}API_FREESTANDING "Builds only freestanding part of the li
set(${projectPrefix}API_CONTRACTS GSL-LITE CACHE STRING "Enable contract checking")
message(STATUS "${projectPrefix}API_STD_FORMAT: ${${projectPrefix}API_STD_FORMAT}")
-message(STATUS "${projectPrefix}API_STRING_VIEW_RET: ${${projectPrefix}API_STRING_VIEW_RET}")
message(STATUS "${projectPrefix}API_NO_CRTP: ${${projectPrefix}API_NO_CRTP}")
message(STATUS "${projectPrefix}API_FREESTANDING: ${${projectPrefix}API_FREESTANDING}")
@@ -107,10 +102,6 @@ if(NOT ${projectPrefix}API_FREESTANDING AND ${projectPrefix}API_STD_FORMAT AND N
message(FATAL_ERROR "`std::format` enabled but not supported")
endif()
-if(${projectPrefix}API_STRING_VIEW_RET AND NOT ${projectPrefix}STATIC_CONSTEXPR_VARS_IN_CONSTEXPR_FUNCTIONS)
- message(FATAL_ERROR "`std::string_view` returns enabled but not supported")
-endif()
-
if(${projectPrefix}API_NO_CRTP AND NOT ${projectPrefix}EXPLICIT_THIS_PARAMETER_SUPPORTED)
message(FATAL_ERROR "`NO_CRTP` mode enabled but explicit `this` parameter is not supported")
endif()
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index fa6c7d9545..1f3dc449d7 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -75,6 +75,7 @@ add_mp_units_module(
include/mp-units/framework/value_cast.h
include/mp-units/compat_macros.h
include/mp-units/concepts.h
+ include/mp-units/core.h
include/mp-units/framework.h
MODULE_INTERFACE_UNIT mp-units-core.cpp
)
@@ -90,15 +91,16 @@ if(NOT ${projectPrefix}API_FREESTANDING)
include/mp-units/bits/fmt.h
include/mp-units/bits/requires_hosted.h
include/mp-units/ext/format.h
+ include/mp-units/cartesian_vector.h
+ include/mp-units/complex.h
+ include/mp-units/format.h
include/mp-units/math.h
include/mp-units/ostream.h
- include/mp-units/format.h
include/mp-units/random.h
)
endif()
set_feature_flag(API_STD_FORMAT)
-set_feature_flag(API_STRING_VIEW_RET)
set_feature_flag(API_NO_CRTP)
# Text formatting
diff --git a/src/core/include/mp-units/bits/core_gmf.h b/src/core/include/mp-units/bits/core_gmf.h
index 5566bc923f..d3a44fcbe5 100644
--- a/src/core/include/mp-units/bits/core_gmf.h
+++ b/src/core/include/mp-units/bits/core_gmf.h
@@ -57,6 +57,7 @@
#ifndef MP_UNITS_IMPORT_STD
#include
#include
+#include
#include
#include
#include
diff --git a/src/core/include/mp-units/bits/get_associated_quantity.h b/src/core/include/mp-units/bits/get_associated_quantity.h
index 56385f8be6..6ec1601064 100644
--- a/src/core/include/mp-units/bits/get_associated_quantity.h
+++ b/src/core/include/mp-units/bits/get_associated_quantity.h
@@ -52,7 +52,7 @@ template
[[nodiscard]] consteval auto all_are_kinds(U)
{
if constexpr (requires { U::_quantity_spec_; })
- return QuantityKindSpec>;
+ return QuantityKindSpec;
else if constexpr (requires { U::_reference_unit_; })
return all_are_kinds(U::_reference_unit_);
else if constexpr (requires { typename U::_num_; }) {
diff --git a/src/core/include/mp-units/bits/hacks.h b/src/core/include/mp-units/bits/hacks.h
index 87d7c47db4..70ee2c1417 100644
--- a/src/core/include/mp-units/bits/hacks.h
+++ b/src/core/include/mp-units/bits/hacks.h
@@ -81,6 +81,12 @@
// workarounds for https://cplusplus.github.io/CWG/issues/2387.html
#define MP_UNITS_INLINE inline
+#if __cpp_auto_cast >= 202110L && MP_UNITS_COMP_GCC != 12
+#define MP_UNITS_NONCONST_TYPE(expr) decltype(auto(expr))
+#else
+#define MP_UNITS_NONCONST_TYPE(expr) std::remove_const_t
+#endif
+
#if MP_UNITS_COMP_GCC
#define MP_UNITS_REMOVE_CONST(expr) std::remove_const_t
@@ -140,12 +146,6 @@ MP_UNITS_DIAGNOSTIC_POP
#endif
-#if !defined MP_UNITS_API_STRING_VIEW_RET && __cpp_constexpr >= 202211L
-
-#define MP_UNITS_API_STRING_VIEW_RET 1
-
-#endif
-
#if !defined MP_UNITS_API_NO_CRTP && __cpp_explicit_this_parameter
#define MP_UNITS_API_NO_CRTP 1
diff --git a/src/core/include/mp-units/bits/sudo_cast.h b/src/core/include/mp-units/bits/sudo_cast.h
index e3e94f9255..1264cc0aef 100644
--- a/src/core/include/mp-units/bits/sudo_cast.h
+++ b/src/core/include/mp-units/bits/sudo_cast.h
@@ -154,8 +154,7 @@ template,
- std::remove_const_t>) {
+ if constexpr (is_same_v) {
return quantity_point{
sudo_cast(std::forward(qp).quantity_from(FromQP::point_origin)),
FromQP::point_origin};
diff --git a/src/core/include/mp-units/cartesian_vector.h b/src/core/include/mp-units/cartesian_vector.h
new file mode 100644
index 0000000000..af5e7e3662
--- /dev/null
+++ b/src/core/include/mp-units/cartesian_vector.h
@@ -0,0 +1,282 @@
+// The MIT License (MIT)
+//
+// Copyright (c) 2018 Mateusz Pusz
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+#pragma once
+
+#include
+//
+#include
+#include
+
+#if MP_UNITS_HOSTED
+#include
+#endif
+
+#ifndef MP_UNITS_IN_MODULE_INTERFACE
+#ifdef MP_UNITS_IMPORT_STD
+import std;
+#else
+#include
+#include
+#include
+#if MP_UNITS_HOSTED
+#include
+#endif
+#endif
+#endif
+
+namespace mp_units {
+
+MP_UNITS_EXPORT template
+class cartesian_vector {
+public:
+ // public members required to satisfy structural type requirements :-(
+ T _coordinates_[3];
+ using value_type = T;
+
+ cartesian_vector(const cartesian_vector&) = default;
+ cartesian_vector(cartesian_vector&&) = default;
+ cartesian_vector& operator=(const cartesian_vector&) = default;
+ cartesian_vector& operator=(cartesian_vector&&) = default;
+
+ template
+ requires(... && std::constructible_from)
+ constexpr explicit(!(... && std::convertible_to)) cartesian_vector(Args&&... args) :
+ _coordinates_{static_cast(std::forward(args))...}
+ {
+ }
+
+ template
+ requires std::constructible_from
+ constexpr explicit(!std::convertible_to) cartesian_vector(const cartesian_vector& other) :
+ _coordinates_{static_cast(other[0]), static_cast(other[1]), static_cast(other[2])}
+ {
+ }
+
+ template
+ requires std::constructible_from
+ constexpr explicit(!std::convertible_to) cartesian_vector(cartesian_vector&& other) :
+ _coordinates_{static_cast(std::move(other[0])), static_cast(std::move(other[1])),
+ static_cast(std::move(other[2]))}
+ {
+ }
+
+ template U>
+ constexpr cartesian_vector& operator=(const cartesian_vector& other)
+ {
+ _coordinates_[0] = other[0];
+ _coordinates_[1] = other[1];
+ _coordinates_[2] = other[2];
+ return *this;
+ }
+
+ template U>
+ constexpr cartesian_vector& operator=(cartesian_vector&& other)
+ {
+ _coordinates_[0] = std::move(other[0]);
+ _coordinates_[1] = std::move(other[1]);
+ _coordinates_[2] = std::move(other[2]);
+ return *this;
+ }
+
+ [[nodiscard]] constexpr T magnitude() const
+ requires treat_as_floating_point
+ {
+ return std::hypot(_coordinates_[0], _coordinates_[1], _coordinates_[2]);
+ }
+
+ [[nodiscard]] constexpr cartesian_vector unit() const
+ requires treat_as_floating_point
+ {
+ return *this / magnitude();
+ }
+
+ [[nodiscard]] constexpr T& operator[](std::size_t i) { return _coordinates_[i]; }
+ [[nodiscard]] constexpr const T& operator[](std::size_t i) const { return _coordinates_[i]; }
+
+ [[nodiscard]] constexpr cartesian_vector operator+() const { return *this; }
+ [[nodiscard]] constexpr cartesian_vector operator-() const
+ {
+ return {-_coordinates_[0], -_coordinates_[1], -_coordinates_[2]};
+ }
+
+ template
+ requires requires(T t, U u) {
+ { t += u } -> std::same_as;
+ }
+ constexpr cartesian_vector& operator+=(const cartesian_vector& other)
+ {
+ _coordinates_[0] += other[0];
+ _coordinates_[1] += other[1];
+ _coordinates_[2] += other[2];
+ return *this;
+ }
+
+ template
+ requires requires(T t, U u) {
+ { t -= u } -> std::same_as;
+ }
+ constexpr cartesian_vector& operator-=(const cartesian_vector& other)
+ {
+ _coordinates_[0] -= other[0];
+ _coordinates_[1] -= other[1];
+ _coordinates_[2] -= other[2];
+ return *this;
+ }
+
+ template
+ requires requires(T t, U u) {
+ { t *= u } -> std::same_as;
+ }
+ constexpr cartesian_vector& operator*=(const U& value)
+ {
+ _coordinates_[0] *= value;
+ _coordinates_[1] *= value;
+ _coordinates_[2] *= value;
+ return *this;
+ }
+
+ template
+ requires requires(T t, U u) {
+ { t /= u } -> std::same_as;
+ }
+ constexpr cartesian_vector& operator/=(const U& value)
+ {
+ _coordinates_[0] /= value;
+ _coordinates_[1] /= value;
+ _coordinates_[2] /= value;
+ return *this;
+ }
+
+ template U, typename V>
+ requires requires(U u, V v) { u + v; }
+ [[nodiscard]] friend constexpr auto operator+(const cartesian_vector& lhs, const cartesian_vector& rhs)
+ {
+ return ::mp_units::cartesian_vector{lhs._coordinates_[0] + rhs._coordinates_[0],
+ lhs._coordinates_[1] + rhs._coordinates_[1],
+ lhs._coordinates_[2] + rhs._coordinates_[2]};
+ }
+
+ template U, typename V>
+ requires requires(U u, V v) { u - v; }
+ [[nodiscard]] friend constexpr auto operator-(const cartesian_vector& lhs, const cartesian_vector& rhs)
+ {
+ return ::mp_units::cartesian_vector{lhs._coordinates_[0] - rhs._coordinates_[0],
+ lhs._coordinates_[1] - rhs._coordinates_[1],
+ lhs._coordinates_[2] - rhs._coordinates_[2]};
+ }
+
+ template U>
+ requires requires(U u, T t) { u* t; }
+ [[nodiscard]] friend constexpr auto operator*(const cartesian_vector& lhs, const T& rhs)
+ {
+ return ::mp_units::cartesian_vector{lhs._coordinates_[0] * rhs, lhs._coordinates_[1] * rhs,
+ lhs._coordinates_[2] * rhs};
+ }
+
+ template U>
+ requires requires(T t, U u) { t* u; }
+ [[nodiscard]] friend constexpr auto operator*(const T& lhs, const cartesian_vector& rhs)
+ {
+ return rhs * lhs;
+ }
+
+ template U>
+ requires requires(U u, T t) { u / t; }
+ [[nodiscard]] friend constexpr auto operator/(const cartesian_vector& lhs, const T& rhs)
+ {
+ return ::mp_units::cartesian_vector{lhs._coordinates_[0] / rhs, lhs._coordinates_[1] / rhs,
+ lhs._coordinates_[2] / rhs};
+ }
+
+ template U, std::equality_comparable_with V>
+ [[nodiscard]] friend constexpr bool operator==(const cartesian_vector& lhs, const cartesian_vector& rhs)
+ {
+ return lhs._coordinates_[0] == rhs._coordinates_[0] && lhs._coordinates_[1] == rhs._coordinates_[1] &&
+ lhs._coordinates_[2] == rhs._coordinates_[2];
+ }
+
+ [[nodiscard]] friend constexpr T norm(const cartesian_vector& vec)
+ requires treat_as_floating_point
+ {
+ return vec.magnitude();
+ }
+
+ [[nodiscard]] friend constexpr cartesian_vector unit_vector(const cartesian_vector& vec)
+ requires treat_as_floating_point
+ {
+ return vec.unit();
+ }
+
+ template U, typename V>
+ requires requires(U u, V v, decltype(u * v) t) {
+ u* v;
+ t + t;
+ }
+ [[nodiscard]] friend constexpr auto scalar_product(const cartesian_vector& lhs, const cartesian_vector& rhs)
+ {
+ return lhs._coordinates_[0] * rhs._coordinates_[0] + lhs._coordinates_[1] * rhs._coordinates_[1] +
+ lhs._coordinates_[2] * rhs._coordinates_[2];
+ }
+
+ template U, typename V>
+ requires requires(U u, V v, decltype(u * v) t) {
+ u* v;
+ t - t;
+ }
+ [[nodiscard]] friend constexpr auto vector_product(const cartesian_vector& lhs, const cartesian_vector& rhs)
+ {
+ return ::mp_units::cartesian_vector{
+ lhs._coordinates_[1] * rhs._coordinates_[2] - lhs._coordinates_[2] * rhs._coordinates_[1],
+ lhs._coordinates_[2] * rhs._coordinates_[0] - lhs._coordinates_[0] * rhs._coordinates_[2],
+ lhs._coordinates_[0] * rhs._coordinates_[1] - lhs._coordinates_[1] * rhs._coordinates_[0]};
+ }
+
+#if MP_UNITS_HOSTED
+ friend constexpr std::ostream& operator<<(std::ostream& os, const cartesian_vector& vec)
+ {
+ return os << '[' << vec[0] << ", " << vec[1] << ", " << vec[2] << ']';
+ }
+#endif
+};
+
+template
+ requires(sizeof...(Args) <= 2) && requires { typename std::common_type_t; }
+cartesian_vector(Arg, Args...) -> cartesian_vector>;
+
+template
+constexpr bool is_vector> = true;
+
+} // namespace mp_units
+
+#if MP_UNITS_HOSTED
+// TODO use parse and use formatter for the underlying type
+template
+struct MP_UNITS_STD_FMT::formatter, Char> :
+ formatter, Char> {
+ template
+ auto format(const mp_units::cartesian_vector& vec, FormatContext& ctx) const
+ {
+ return format_to(ctx.out(), "[{}, {}, {}]", vec[0], vec[1], vec[2]);
+ }
+};
+#endif
diff --git a/src/core/include/mp-units/complex.h b/src/core/include/mp-units/complex.h
new file mode 100644
index 0000000000..9f09c05f75
--- /dev/null
+++ b/src/core/include/mp-units/complex.h
@@ -0,0 +1,43 @@
+// The MIT License (MIT)
+//
+// Copyright (c) 2018 Mateusz Pusz
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+#pragma once
+
+#include
+//
+#include
+#include
+
+#ifndef MP_UNITS_IN_MODULE_INTERFACE
+#ifdef MP_UNITS_IMPORT_STD
+import std;
+#else
+#include
+#endif
+#endif
+
+namespace mp_units {
+
+template
+constexpr bool is_complex> = true;
+
+}
diff --git a/src/core/include/mp-units/core.h b/src/core/include/mp-units/core.h
index 09ba3ac379..702f3b6b23 100644
--- a/src/core/include/mp-units/core.h
+++ b/src/core/include/mp-units/core.h
@@ -28,6 +28,8 @@
#include
#if MP_UNITS_HOSTED
+#include
+#include
#include
#include
#include
diff --git a/src/core/include/mp-units/ext/inplace_vector.h b/src/core/include/mp-units/ext/inplace_vector.h
index 1e93caf4d2..8074b09b5e 100644
--- a/src/core/include/mp-units/ext/inplace_vector.h
+++ b/src/core/include/mp-units/ext/inplace_vector.h
@@ -90,16 +90,16 @@ class inplace_vector {
constexpr T* data() noexcept { return data_; }
constexpr const T* data() const noexcept { return data_; }
- constexpr reference push_back(const T& v)
+ constexpr reference push_back(const T& val)
requires std::constructible_from
{
- return emplace_back(v);
+ return emplace_back(val);
}
- constexpr reference push_back(T&& v)
+ constexpr reference push_back(T&& val)
requires std::constructible_from
{
- return emplace_back(std::forward(v));
+ return emplace_back(std::forward(val));
}
template
diff --git a/src/core/include/mp-units/ext/prime.h b/src/core/include/mp-units/ext/prime.h
index 5b372fe958..a5a8f10687 100644
--- a/src/core/include/mp-units/ext/prime.h
+++ b/src/core/include/mp-units/ext/prime.h
@@ -28,6 +28,7 @@
#include
#ifndef MP_UNITS_IN_MODULE_INTERFACE
+#include
#ifdef MP_UNITS_IMPORT_STD
import std;
#else
@@ -37,76 +38,382 @@ import std;
#include
#include
#include
+#include
#endif
#endif
namespace mp_units::detail {
-[[nodiscard]] consteval bool is_prime_by_trial_division(std::uintmax_t n)
+// (a + b) % n.
+//
+// Precondition: (a < n).
+// Precondition: (b < n).
+// Precondition: (n > 0).
+[[nodiscard]] consteval std::uint64_t add_mod(std::uint64_t a, std::uint64_t b, std::uint64_t n)
{
- for (std::uintmax_t f = 2; f * f <= n; f += 1 + (f % 2)) {
- if (n % f == 0) {
- return false;
+ MP_UNITS_EXPECTS_DEBUG(a < n);
+ MP_UNITS_EXPECTS_DEBUG(b < n);
+ MP_UNITS_EXPECTS_DEBUG(n > 0u);
+
+ if (a >= n - b) {
+ return a - (n - b);
+ } else {
+ return a + b;
+ }
+}
+
+// (a - b) % n.
+//
+// Precondition: (a < n).
+// Precondition: (b < n).
+// Precondition: (n > 0).
+[[nodiscard]] consteval std::uint64_t sub_mod(std::uint64_t a, std::uint64_t b, std::uint64_t n)
+{
+ MP_UNITS_EXPECTS_DEBUG(a < n);
+ MP_UNITS_EXPECTS_DEBUG(b < n);
+ MP_UNITS_EXPECTS_DEBUG(n > 0u);
+
+ if (a >= b) {
+ return a - b;
+ } else {
+ return n - (b - a);
+ }
+}
+
+// (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_EXPECTS_DEBUG(a < n);
+ MP_UNITS_EXPECTS_DEBUG(b < n);
+ MP_UNITS_EXPECTS_DEBUG(n > 0u);
+
+ if (b == 0u || a < std::numeric_limits::max() / b) {
+ return (a * b) % n;
+ }
+
+ const std::uint64_t batch_size = n / a;
+ const std::uint64_t num_batches = b / batch_size;
+
+ 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,
+
+ // Handle the leftover product (which is guaranteed to fit in the integer type).
+ (a * (b % batch_size)) % n,
+
+ n);
+}
+
+// (a / 2) % n.
+//
+// Precondition: (a < n).
+// Precondition: (n % 2 == 1).
+[[nodiscard]] consteval std::uint64_t half_mod_odd(std::uint64_t a, std::uint64_t n)
+{
+ MP_UNITS_EXPECTS_DEBUG(a < n);
+ MP_UNITS_EXPECTS_DEBUG(n % 2 == 1);
+
+ return (a / 2u) + ((a % 2u == 0u) ? 0u : (n / 2u + 1u));
+}
+
+// (base ^ exp) % n.
+[[nodiscard]] consteval std::uint64_t pow_mod(std::uint64_t base, std::uint64_t exp, std::uint64_t n)
+{
+ std::uint64_t result = 1u;
+ base %= n;
+
+ while (exp > 0u) {
+ if (exp % 2u == 1u) {
+ result = mul_mod(result, base, n);
}
+
+ exp /= 2u;
+ base = mul_mod(base, base, n);
}
- return true;
+
+ return result;
}
-// Return the first factor of n, as long as it is either k or n.
+// Decompose a positive integer by factoring out all powers of 2.
+struct NumberDecomposition {
+ std::uint64_t power_of_two;
+ std::uint64_t odd_remainder;
+};
+
+// Express any positive `n` as `(2^s * d)`, where `d` is odd.
+[[nodiscard]] consteval NumberDecomposition decompose(std::uint64_t n)
+{
+ NumberDecomposition result{0u, n};
+ while (result.odd_remainder % 2u == 0u) {
+ result.odd_remainder /= 2u;
+ ++result.power_of_two;
+ }
+ return result;
+}
+
+// Perform a Miller-Rabin primality test on `n` using base `a`.
//
-// Precondition: no integer smaller than k evenly divides n.
-[[nodiscard]] constexpr std::optional first_factor_maybe(std::uintmax_t n, std::uintmax_t k)
+// Precondition: (a >= 2).
+// Precondition: (n >= a + 2).
+// Precondition: (n % 2 == 1).
+[[nodiscard]] consteval bool miller_rabin_probable_prime(std::uint64_t a, std::uint64_t n)
{
- if (n % k == 0) {
- return k;
+ MP_UNITS_EXPECTS_DEBUG(a >= 2u);
+ MP_UNITS_EXPECTS_DEBUG(n >= a + 2u);
+ MP_UNITS_EXPECTS_DEBUG(n % 2u == 1u);
+
+ const auto [s, d] = decompose(n - 1u);
+ auto x = pow_mod(a, d, n);
+ if (x == 1u) {
+ return true;
}
- if (k * k > n) {
- return n;
+
+ const auto minus_one = n - 1u;
+ for (auto r = 0u; r < s; ++r) {
+ if (x == minus_one) {
+ return true;
+ }
+ x = mul_mod(x, x, n);
}
- return std::nullopt;
+
+ return false;
}
-template
-[[nodiscard]] consteval std::array first_n_primes()
+// The Jacobi symbol, notated as `(a/n)`, is defined for odd positive `n` and any integer `a`, taking values
+// in the set `{-1, 0, 1}`. Besides being a completely multiplicative function (so that, for example, both
+// (a*b/n) = (a/n) * (b/n), and (a/n*m) = (a/n) * (a/m)), it obeys the following symmetry rules, which enable
+// its calculation:
+//
+// 1. (a/1) = 1, and (1/n) = 1, for all a and n.
+//
+// 2. (a/n) = 0 whenever a and n have a nontrivial common factor.
+//
+// 3. (a/n) = (b/n) whenever (a % n) = (b % n).
+//
+// 4. (2a/n) = (a/n) if n % 8 = 1 or 7, and -(a/n) if n % 8 = 3 or 5.
+//
+// 5. (a/n) = (n/a) * x if a and n are both odd, positive, and coprime. Here, x is 1 if either (a % 4) = 1
+// or (n % 4) = 1, and -1 otherwise.
+//
+// 6. (-1/n) = 1 if n % 4 = 1, and -1 if n % 4 = 3.
+[[nodiscard]] consteval int jacobi_symbol(std::int64_t raw_a, std::uint64_t n)
{
- std::array primes{};
- primes[0] = 2;
- for (std::size_t i = 1; i < N; ++i) {
- primes[i] = primes[i - 1] + 1;
- while (!is_prime_by_trial_division(primes[i])) {
- ++primes[i];
+ // Rule 1: n=1 case.
+ if (n == 1u) {
+ return 1;
+ }
+
+ // Starting conditions: transform `a` to strictly non-negative values, setting `result` to the sign that we
+ // pick up (if any) from following these rules (i.e., rules 3 and 6).
+ int result = ((raw_a >= 0) || (n % 4u == 1u)) ? 1 : -1;
+ auto a = static_cast(raw_a < 0 ? -raw_a : raw_a) % n;
+
+ while (a != 0u) {
+ // Rule 4.
+ const int sign_for_even = (n % 8u == 1u || n % 8u == 7u) ? 1 : -1;
+ while (a % 2u == 0u) {
+ a /= 2u;
+ result *= sign_for_even;
+ }
+
+ // Rule 1: a=1 case.
+ if (a == 1u) {
+ return result;
+ }
+
+ // Rule 2.
+ if (std::gcd(a, n) != 1u) {
+ return 0;
}
+
+ // Note that at this point, we know that `a` and `n` are coprime, and are both odd and positive.
+ // Therefore, we meet the preconditions for rule 5 (the "flip-and-reduce" rule).
+ result *= (n % 4u == 1u || a % 4u == 1u) ? 1 : -1;
+ const std::uint64_t new_a = n % a;
+ n = a;
+ a = new_a;
}
- return primes;
+
+ return 0;
}
-template
-consteval void call_for_coprimes_up_to(std::uintmax_t n, const std::array& basis, Callable call)
+[[nodiscard]] consteval bool is_perfect_square(std::uint64_t n)
{
- for (std::uintmax_t i = 0u; i < n; ++i) {
- if (std::apply([&i](auto... primes) { return ((i % primes != 0) && ...); }, basis)) {
- call(i);
+ if (n < 2u) {
+ return true;
+ }
+
+ std::uint64_t prev = n / 2u;
+ while (true) {
+ const std::uint64_t curr = (prev + n / prev) / 2u;
+ if (curr * curr == n) {
+ return true;
}
+ if (curr >= prev) {
+ return false;
+ }
+ prev = curr;
}
}
-template
-[[nodiscard]] consteval std::size_t num_coprimes_up_to(std::uintmax_t n, const std::array& basis)
+// The "D" parameter in the Strong Lucas Probable Prime test.
+//
+// Default construction produces the first value to try, according to Selfridge's parameter selection.
+// Calling `successor()` on this repeatedly will produce the sequence of values to try.
+struct LucasDParameter {
+ std::uint64_t mag = 5u;
+ bool pos = true;
+
+ friend constexpr int as_int(LucasDParameter p)
+ {
+ int D = static_cast(p.mag);
+ return p.pos ? D : -D;
+ }
+ friend constexpr LucasDParameter successor(LucasDParameter p) { return {.mag = p.mag + 2u, .pos = !p.pos}; }
+};
+
+// The first `D` in the infinite sequence {5, -7, 9, -11, ...} whose Jacobi symbol is -1. This is the D we
+// want to use for the Strong Lucas Probable Prime test.
+//
+// Precondition: `n` is not a perfect square.
+[[nodiscard]] consteval LucasDParameter find_first_D_with_jacobi_symbol_neg_one(std::uint64_t n)
+{
+ LucasDParameter D{};
+ while (jacobi_symbol(as_int(D), n) != -1) {
+ D = successor(D);
+ }
+ return D;
+}
+
+// An element of the Lucas sequence, with parameters tuned for the Strong Lucas test.
+//
+// The default values give the first element (i.e., k=1) of the sequence.
+struct LucasSequenceElement {
+ std::uint64_t u = 1u;
+ std::uint64_t v = 1u;
+};
+
+// Produce the Lucas element whose index is twice the input element's index.
+[[nodiscard]] consteval LucasSequenceElement double_strong_lucas_index(const LucasSequenceElement& element,
+ std::uint64_t n, LucasDParameter D)
+{
+ const auto& [u, v] = element;
+
+ std::uint64_t v_squared = mul_mod(v, v, n);
+ std::uint64_t D_u_squared = mul_mod(D.mag % n, mul_mod(u, u, n), n);
+ std::uint64_t new_v = D.pos ? add_mod(v_squared, D_u_squared, n) : sub_mod(v_squared, D_u_squared, n);
+ new_v = half_mod_odd(new_v, n);
+
+ return {.u = mul_mod(u, v, n), .v = new_v};
+}
+
+[[nodiscard]] consteval LucasSequenceElement increment_strong_lucas_index(const LucasSequenceElement& element,
+ std::uint64_t n, LucasDParameter D)
+{
+ const auto& [u, v] = element;
+
+ const auto new_u = half_mod_odd(add_mod(u, v, n), n);
+
+ const auto D_u = mul_mod(D.mag % n, u, n);
+ auto new_v = D.pos ? add_mod(v, D_u, n) : sub_mod(v, D_u, n);
+ new_v = half_mod_odd(new_v, n);
+
+ return {.u = new_u, .v = new_v};
+}
+
+[[nodiscard]] consteval LucasSequenceElement find_strong_lucas_element(std::uint64_t i, std::uint64_t n,
+ LucasDParameter D)
{
- std::size_t count = 0u;
- call_for_coprimes_up_to(n, basis, [&count](auto) { ++count; });
- return count;
+ LucasSequenceElement element{};
+
+ bool bits[64] = {};
+ std::size_t n_bits = 0u;
+ while (i > 1u) {
+ bits[n_bits++] = (i & 1u);
+ i >>= 1u;
+ }
+
+ for (auto j = n_bits; j > 0u; --j) {
+ element = double_strong_lucas_index(element, n, D);
+ if (bits[j - 1u]) {
+ element = increment_strong_lucas_index(element, n, D);
+ }
+ }
+
+ return element;
}
-template
-[[nodiscard]] consteval auto coprimes_up_to(std::size_t n, const std::array& basis)
+// Perform a Strong Lucas Probable Prime test on `n`.
+//
+// Precondition: (n >= 2).
+// Precondition: (n is odd).
+[[nodiscard]] consteval bool strong_lucas_probable_prime(std::uint64_t n)
{
- std::array coprimes{};
- std::size_t i = 0u;
+ MP_UNITS_EXPECTS_DEBUG(n >= 2u);
+ MP_UNITS_EXPECTS_DEBUG(n % 2u == 1u);
+
+ if (is_perfect_square(n)) {
+ return false;
+ }
+
+ const auto D = find_first_D_with_jacobi_symbol_neg_one(n);
- call_for_coprimes_up_to(n, basis, [&coprimes, &i](std::uintmax_t cp) { coprimes[i++] = cp; });
+ const auto [s, d] = decompose(n + 1u);
- return coprimes;
+ auto element = find_strong_lucas_element(d, n, D);
+ if (element.u == 0u) {
+ return true;
+ }
+
+ for (auto i = 0u; i < s; ++i) {
+ if (element.v == 0u) {
+ return true;
+ }
+ element = double_strong_lucas_index(element, n, D);
+ }
+
+ return false;
+}
+
+// The Baillie-PSW test is technically a "probable prime" test. However, it is known to be correct for all
+// 64-bit integers, and no counterexample of any size has ever been found. Thus, for this library's purposes,
+// we can treat it as deterministic... probably.
+[[nodiscard]] consteval bool baillie_psw_probable_prime(std::uint64_t n)
+{
+ if (n < 2u) {
+ return false;
+ }
+ if (n < 4u) {
+ return true;
+ }
+ if (n % 2u == 0u) {
+ return false;
+ }
+
+ if (!miller_rabin_probable_prime(2u, n)) {
+ return false;
+ }
+
+ return strong_lucas_probable_prime(n);
+}
+
+template
+[[nodiscard]] consteval std::array first_n_primes()
+{
+ std::array primes{};
+ primes[0] = 2;
+ for (std::size_t i = 1; i < N; ++i) {
+ primes[i] = primes[i - 1] + 1;
+ while (!baillie_psw_probable_prime(primes[i])) {
+ ++primes[i];
+ }
+ }
+ return primes;
}
template
@@ -135,48 +442,45 @@ constexpr auto get_first_of(const Rng& rng, UnaryFunction f)
return get_first_of(begin(rng), end(rng), f);
}
-// A configurable instantiation of the "wheel factorization" algorithm [1] for prime numbers.
-//
-// Instantiate with N to use a "basis" of the first N prime numbers. Higher values of N use fewer trial divisions, at
-// the cost of additional space. The amount of space consumed is roughly the total number of numbers that are a) less
-// than the _product_ of the basis elements (first N primes), and b) coprime with every element of the basis. This
-// means it grows rapidly with N. Consider this approximate chart:
-//
-// N | Num coprimes | Trial divisions needed
-// --+--------------+-----------------------
-// 1 | 1 | 50.0 %
-// 2 | 2 | 33.3 %
-// 3 | 8 | 26.7 %
-// 4 | 48 | 22.9 %
-// 5 | 480 | 20.8 %
-//
-// Note the diminishing returns, and the rapidly escalating costs. Consider this behaviour when choosing the value of N
-// most appropriate for your needs.
-//
-// [1] https://en.wikipedia.org/wiki/Wheel_factorization
-template
-struct wheel_factorizer {
- static constexpr auto basis = first_n_primes();
- static constexpr auto wheel_size = product(basis);
- static constexpr auto coprimes_in_first_wheel =
- coprimes_up_to(wheel_size, basis);
+#if __cpp_constexpr < 202211L
+template
+struct first_n_primes_impl {
+ static constexpr auto value = first_n_primes();
+};
+#endif
- [[nodiscard]] static consteval std::uintmax_t find_first_factor(std::uintmax_t n)
- {
- if (const auto k = detail::get_first_of(basis, [&](auto p) { return first_factor_maybe(n, p); })) return *k;
+[[nodiscard]] consteval std::uintmax_t find_first_factor(std::uintmax_t n)
+{
+#if __cpp_constexpr >= 202211L
+ static constexpr auto first_100_primes = first_n_primes<100>();
+#else
+ constexpr auto first_100_primes = first_n_primes_impl<100>::value;
+#endif
- if (const auto k = detail::get_first_of(std::next(begin(coprimes_in_first_wheel)), end(coprimes_in_first_wheel),
- [&](auto p) { return first_factor_maybe(n, p); }))
- return *k;
+ for (const auto& p : first_100_primes) {
+ if (n % p == 0u) {
+ return p;
+ }
+ if (p * p > n) {
+ return n;
+ }
+ }
- for (std::size_t wheel = wheel_size; wheel < n; wheel += wheel_size)
- if (const auto k =
- detail::get_first_of(coprimes_in_first_wheel, [&](auto p) { return first_factor_maybe(n, wheel + p); }))
- return *k;
+ // If we got this far, and we haven't found a factor, and haven't terminated... maybe it's prime? Do a fast check.
+ if (baillie_psw_probable_prime(n)) {
return n;
}
- [[nodiscard]] static consteval bool is_prime(std::size_t n) { return (n > 1) && find_first_factor(n) == n; }
-};
+ // If we're here, we know `n` is composite, so continue with trial division for all odd numbers.
+ std::uintmax_t factor = first_100_primes.back() + 2u;
+ while (factor * factor <= n) {
+ if (n % factor == 0u) {
+ return factor;
+ }
+ factor += 2u;
+ }
+
+ return n; // Technically unreachable.
+}
} // namespace mp_units::detail
diff --git a/src/core/include/mp-units/ext/type_traits.h b/src/core/include/mp-units/ext/type_traits.h
index 70b9523c74..119a27c555 100644
--- a/src/core/include/mp-units/ext/type_traits.h
+++ b/src/core/include/mp-units/ext/type_traits.h
@@ -193,11 +193,4 @@ template typename T, typename T1, typename T2, typename... Ts>
return get();
}
-namespace detail {
-
-template
-concept SymbolicConstant = std::is_empty_v && std::is_final_v;
-
-}
-
} // namespace mp_units
diff --git a/src/core/include/mp-units/format.h b/src/core/include/mp-units/format.h
index fe19a07e51..a99a310157 100644
--- a/src/core/include/mp-units/format.h
+++ b/src/core/include/mp-units/format.h
@@ -291,8 +291,8 @@ class MP_UNITS_STD_FMT::formatter, Char> {
static constexpr auto dimension = get_quantity_spec(Reference).dimension;
using quantity_t = mp_units::quantity;
- using unit_t = std::remove_const_t;
- using dimension_t = std::remove_const_t;
+ using unit_t = MP_UNITS_NONCONST_TYPE(unit);
+ using dimension_t = MP_UNITS_NONCONST_TYPE(dimension);
using format_specs = mp_units::detail::fill_align_width_format_specs;
format_specs specs_{};
diff --git a/src/core/include/mp-units/framework/construction_helpers.h b/src/core/include/mp-units/framework/construction_helpers.h
index 43eade949a..36ffac6eb6 100644
--- a/src/core/include/mp-units/framework/construction_helpers.h
+++ b/src/core/include/mp-units/framework/construction_helpers.h
@@ -39,7 +39,7 @@ namespace mp_units {
template
struct delta_ {
- template Rep = std::remove_cvref_t>
+ template Rep = std::remove_cvref_t>
[[nodiscard]] constexpr quantity operator()(FwdRep&& lhs) const
{
return quantity{std::forward(lhs), R{}};
@@ -48,7 +48,7 @@ struct delta_ {
template
struct absolute_ {
- template Rep = std::remove_cvref_t>
+ template Rep = std::remove_cvref_t>
[[nodiscard]] constexpr quantity_point
operator()(FwdRep&& lhs) const
{
diff --git a/src/core/include/mp-units/framework/dimension.h b/src/core/include/mp-units/framework/dimension.h
index b39e38d9aa..73223ccbfd 100644
--- a/src/core/include/mp-units/framework/dimension.h
+++ b/src/core/include/mp-units/framework/dimension.h
@@ -52,7 +52,7 @@ import std;
namespace mp_units {
-template
+template