From a4a8ef45bc7514f2fc717a733821bf070a69dd85 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Mon, 20 Jul 2026 14:22:57 -0700 Subject: [PATCH 01/13] fix(moves)!: make Metropolis transitions reversible and reproducible - complete failure-atomic 3D Pachner moves and Hastings proposal ratios - centralize seeded PCG streams across initialization and transitions - standardize local and GitHub Actions validation through just ci and Semgrep - synchronize v1.0.0-rc1 metadata, citations, and uv experiment entry points BREAKING CHANGE: move helpers now require caller-owned RNG state, and generated OFF filenames include seed and pass metadata. Closes #92 Closes #105 --- .github/CONTRIBUTING.md | 12 +- .github/workflows/ci.yml | 88 +-- .semgrepignore | 6 + CITATION.cff | 24 + CMakeLists.txt | 17 +- CMakePresets.json | 10 +- Justfile | 131 ++-- README.md | 100 ++- REFERENCES.md | 73 ++ cmake/RunCdtNoOutputTest.cmake | 9 +- cmake/Version.hpp.in | 17 + docs/Doxyfile | 2 +- docs/metropolis-hastings.md | 107 +++ docs/reproducibility.md | 60 ++ include/Apply_move.hpp | 24 +- include/Ergodic_moves_3.hpp | 319 +++++---- include/Foliated_triangulation.hpp | 54 +- include/Manifold.hpp | 24 +- include/Metropolis.hpp | 463 ++++++------ include/Move_always.hpp | 39 +- include/Move_command.hpp | 65 +- include/Move_tracker.hpp | 17 +- include/Mpfr_value.hpp | 39 +- include/Random.hpp | 96 +++ include/S3Action.hpp | 47 +- include/Utilities.hpp | 105 ++- pyproject.toml | 42 +- scripts/__init__.py | 1 + scripts/clang-tidy.sh | 12 +- scripts/mnist_experiment.py | 64 ++ scripts/optimize_initialize.py | 151 ++++ scripts/release_check.py | 225 ++++++ scripts/semgrep_fixture_config.py | 75 ++ scripts/tests/__init__.py | 1 + scripts/tests/test_optimize_initialize.py | 38 + scripts/tests/test_release_check.py | 91 +++ semgrep.yaml | 203 ++++++ src/CMakeLists.txt | 55 +- src/cdt-viewer.cpp | 81 +-- src/cdt.cpp | 26 +- src/initialize.cpp | 22 +- src/optimize-initialize.py | 147 ---- src/test.py | 45 -- tests/Apply_move_test.cpp | 294 ++++---- tests/CMakeLists.txt | 38 +- tests/Ergodic_moves_3_test.cpp | 142 ++-- tests/Foliated_triangulation_test.cpp | 182 +---- tests/Function_ref_test.cpp | 49 +- tests/Geometry_test.cpp | 33 +- tests/Manifold_test.cpp | 155 ++-- tests/Metropolis_test.cpp | 497 +++++++++++-- tests/Move_always_test.cpp | 32 +- tests/Move_command_test.cpp | 137 ++-- tests/Random_benchmark.cpp | 82 +++ tests/Random_header_consumer.cpp | 5 + tests/Random_test.cpp | 117 ++++ tests/S3Action_test.cpp | 35 +- tests/Settings_test.cpp | 18 +- tests/Tetrahedron_test.cpp | 35 +- tests/Utilities_test.cpp | 168 +++-- tests/Vertex_test.cpp | 37 +- tests/main.cpp | 1 - tests/semgrep/doctest_hygiene.cpp | 38 + tests/semgrep/move_hot_path_logging.cpp | 19 + tests/semgrep/random_ownership.cpp | 30 + uv.lock | 814 +++++++++++++++++++++- vcpkg.json | 2 +- 67 files changed, 4305 insertions(+), 1882 deletions(-) create mode 100644 .semgrepignore create mode 100644 CITATION.cff create mode 100644 REFERENCES.md create mode 100644 cmake/Version.hpp.in create mode 100644 docs/metropolis-hastings.md create mode 100644 docs/reproducibility.md create mode 100644 include/Random.hpp create mode 100644 scripts/__init__.py create mode 100644 scripts/mnist_experiment.py create mode 100644 scripts/optimize_initialize.py create mode 100755 scripts/release_check.py create mode 100755 scripts/semgrep_fixture_config.py create mode 100644 scripts/tests/__init__.py create mode 100644 scripts/tests/test_optimize_initialize.py create mode 100644 scripts/tests/test_release_check.py create mode 100644 semgrep.yaml delete mode 100644 src/optimize-initialize.py delete mode 100644 src/test.py create mode 100644 tests/Random_benchmark.cpp create mode 100644 tests/Random_header_consumer.cpp create mode 100644 tests/Random_test.cpp create mode 100644 tests/semgrep/doctest_hygiene.cpp create mode 100644 tests/semgrep/move_hot_path_logging.cpp create mode 100644 tests/semgrep/random_ownership.cpp diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 46d1f11cc1..487ee6e00b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -2,7 +2,8 @@ Thank you for helping improve CDT++. -CDT++ is being prepared for one final C++23 release, v1.0.0, after which this repository will be archived. The project +CDT++ v1.0.0-rc1 is the release candidate for the final C++23 release, v1.0.0, after which this repository will be +archived. The project is maintained as a scientific reference implementation and regression oracle for [causal-triangulations](https://github.com/acgetchell/causal-triangulations), its supported Rust successor. The maintenance and archival scope is tracked by @@ -46,9 +47,12 @@ substantial implementation so its maintenance value and scope can be agreed upon just ci ``` - `just check` is the fast, non-mutating source and tooling gate. `just ci` adds the supported build and 22-test smoke - suite. When changing C++ behavior, also run `just clang-tidy` with the pinned LLVM 22 toolchain and review its - advisory diagnostics. + `just check` is the fast, non-mutating source and tooling gate, including the repository-owned Semgrep policy and + its fixtures. `just ci` adds the supported build and complete 21-entry CTest suite: all 83 doctest unit scenarios + and 20 CLI integration tests. When changing C++ behavior, also run + `just clang-tidy` with the pinned LLVM 22 toolchain and review its advisory diagnostics. + GitHub Actions runs the same `just ci` contract in its Ubuntu GCC, Ubuntu Clang, macOS AppleClang, and Windows + MSVC jobs. The Windows job continues to compile with native MSVC; LLVM tooling is used only for source formatting. 6. Run the relevant Linux sanitizer configuration for changes involving memory, lifetime, undefined behavior, or concurrency: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb83044b2b..64801be272 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,10 +44,17 @@ jobs: - name: Windows MSVC os: windows-latest compiler_package: "" - cc: "" - cxx: "" + cc: cl + cxx: cl steps: + - name: Disable Git autocrlf on Windows + if: runner.os == 'Windows' + shell: pwsh + run: | + git config --global core.autocrlf false + git config --global core.eol lf + - name: Check out repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: @@ -55,58 +62,16 @@ jobs: persist-credentials: false submodules: true - - name: Set up Unix environment with pkgx - if: runner.os != 'Windows' - uses: pkgxdev/setup@4d4ae97af87ccb39ab8be4e073dea697fef2c6f7 # v5.0.0 - - name: Activate MSVC if: runner.os == 'Windows' uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0 with: arch: x64 - - name: Select the Windows vcpkg triplet - if: runner.os == 'Windows' - shell: pwsh - run: Add-Content -Path $env:GITHUB_ENV -Value 'VCPKG_DEFAULT_TRIPLET=x64-windows' - - - name: Restore artifacts or set up vcpkg - uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11.6 - - - name: Build and test Unix - if: runner.os != 'Windows' - env: - CC: ${{ matrix.cc }} - CDT_PKGX_COMPILER_PACKAGE: ${{ matrix.compiler_package }} - CXX: ${{ matrix.cxx }} - run: ./scripts/pkgx-build.sh - - - name: Configure Windows - if: runner.os == 'Windows' - run: cmake --preset reference -S . - - - name: Build Windows - if: runner.os == 'Windows' - run: cmake --build --preset reference --parallel 2 - - - name: Test Windows - if: runner.os == 'Windows' - run: ctest --preset reference-smoke - - quality: - name: Repository quality checks - runs-on: ubuntu-latest - - steps: - - name: Check out repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - name: Set up Just uses: ./.github/actions/setup-just - - name: Resolve tool versions + - name: Resolve CI tool versions id: tool-versions shell: bash run: | @@ -116,7 +81,8 @@ jobs: echo "zizmor=$(just --evaluate zizmor_version)" } >> "$GITHUB_OUTPUT" - - name: Set up repository tools with pkgx + - name: Set up canonical CI environment with pkgx + if: runner.os != 'Windows' uses: pkgxdev/setup@4d4ae97af87ccb39ab8be4e073dea697fef2c6f7 # v5.0.0 with: +: llvm.org@${{ steps.tool-versions.outputs.llvm }} @@ -139,22 +105,42 @@ jobs: with: tool: zizmor@${{ steps.tool-versions.outputs.zizmor }} - - name: Check repository quality - run: just check + - name: Install pinact on Windows + if: runner.os == 'Windows' + shell: pwsh + run: | + $installDirectory = Join-Path $env:RUNNER_TEMP 'pinact-bin' + New-Item -ItemType Directory -Force -Path $installDirectory | Out-Null + $env:GOBIN = $installDirectory + $pinactModule = just --evaluate pinact_module + go install $pinactModule + $installDirectory | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Select the Windows vcpkg triplet + if: runner.os == 'Windows' + shell: pwsh + run: Add-Content -Path $env:GITHUB_ENV -Value 'VCPKG_DEFAULT_TRIPLET=x64-windows' + + - name: Restore artifacts or set up vcpkg + uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11.6 + + - name: Run the canonical local CI contract + env: + CC: ${{ matrix.cc }} + CDT_PKGX_COMPILER_PACKAGE: ${{ matrix.compiler_package }} + CXX: ${{ matrix.cxx }} + run: just ci build: name: build if: ${{ always() }} needs: - platform - - quality runs-on: ubuntu-latest steps: - name: Verify platform matrix env: PLATFORM_RESULT: ${{ needs.platform.result }} - QUALITY_RESULT: ${{ needs.quality.result }} run: | test "${PLATFORM_RESULT}" = success - test "${QUALITY_RESULT}" = success diff --git a/.semgrepignore b/.semgrepignore new file mode 100644 index 0000000000..9aab1819ff --- /dev/null +++ b/.semgrepignore @@ -0,0 +1,6 @@ +# CDT++ intentionally scans tests/; Semgrep's built-in defaults do not. +.git/ +.cache/ +.venv/ +out/ +vcpkg_installed/ diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000000..0d268d3f34 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,24 @@ +cff-version: 1.2.0 +message: "If you use this software, please cite it as below." +type: software +title: "CDT-plusplus: Causal Dynamical Triangulations in C++" +abstract: >- + A C++23 scientific reference implementation of 2+1-dimensional causal + dynamical triangulations, including foliated triangulation construction, + Pachner moves, the Regge action, and Metropolis-Hastings evolution. +authors: + - family-names: "Getchell" + given-names: "Adam" + email: "adam@adamgetchell.org" + orcid: "https://orcid.org/0000-0002-0797-0021" +version: "1.0.0-rc1" +date-released: "2026-07-20" +repository-code: "https://github.com/acgetchell/CDT-plusplus" +url: "https://github.com/acgetchell/CDT-plusplus" +license: BSD-3-Clause +keywords: + - "causal-dynamical-triangulations" + - "computational-geometry" + - "metropolis-hastings" + - "quantum-gravity" + - "regge-calculus" diff --git a/CMakeLists.txt b/CMakeLists.txt index b6912a486c..4fe1111d87 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,10 +22,16 @@ unset(_vcpkg_install_options) project( CDT-plusplus - VERSION 0.1.8 + VERSION 1.0.0 DESCRIPTION "Fast Causal Dynamical Triangulations in C++" LANGUAGES CXX) +# CMake project versions are numeric, so keep the release-candidate suffix in +# the product version used by executables and release metadata. +set(CDT_VERSION_SUFFIX "-rc1") +set(CDT_VERSION "${PROJECT_VERSION}${CDT_VERSION_SUFFIX}") +configure_file(cmake/Version.hpp.in "${PROJECT_BINARY_DIR}/Version.hpp" @ONLY) + # Project settings include(cmake/StandardProjectSettings.cmake) @@ -114,7 +120,8 @@ find_package(fmt CONFIG REQUIRED) find_package(Microsoft.GSL CONFIG REQUIRED) # https://www.pcg-random.org -find_path(PCG_INCLUDE_DIRS "pcg_extras.hpp") +find_path(PCG_INCLUDE_DIRS "pcg_extras.hpp" REQUIRED) +target_include_directories(project_options SYSTEM INTERFACE "${PCG_INCLUDE_DIRS}") find_package(Boost CONFIG REQUIRED COMPONENTS compat program_options) target_link_libraries(project_options INTERFACE Boost::compat) @@ -126,14 +133,12 @@ find_package(spdlog CONFIG REQUIRED) find_package(TBB CONFIG REQUIRED) # Header files -include_directories(BEFORE ${PROJECT_SOURCE_DIR}/include) +include_directories(BEFORE ${PROJECT_BINARY_DIR} ${PROJECT_SOURCE_DIR}/include) # doctest if(ENABLE_TESTING) enable_testing() - message(STATUS "Building tests. Look at /tests for unit tests.") - message(STATUS "Look at /tests/logs for spdlog results from unit tests.") - message(NOTICE "These logs can get quite big in DEBUG mode if you run all tests.") + message(STATUS "Building tests.") add_subdirectory(tests) endif() diff --git a/CMakePresets.json b/CMakePresets.json index a7da6bbb15..a245e978e4 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -102,18 +102,12 @@ "testPresets": [ { "name": "reference-smoke", - "displayName": "Run the supported reference smoke tests", - "description": "Exclude the full and duplicate unit-test registrations", + "displayName": "Run the complete unit and integration suite", + "description": "Run all registered unit and integration tests", "configurePreset": "reference", "output": { "outputOnFailure": true }, - "filter": { - "exclude": { - "name": "^(cdt-unit-tests|cdt-utilities-tests)$", - "label": "reference-smoke-exclude" - } - }, "execution": { "noTestsAction": "error", "stopOnFailure": false diff --git a/Justfile b/Justfile index 489d5f7169..e110057c23 100644 --- a/Justfile +++ b/Justfile @@ -1,24 +1,26 @@ # Justfile for the CDT++ maintenance workflow. # Usage: just or just --list -set minimum-version := "1.56.0" +set minimum-version := "1.57.0" +set shell := ["bash", "-euo", "pipefail", "-c"] -just_version := "1.56.0" +just_version := "1.57.0" uv_version := "0.11.29" pinact_version := "4.1.0" pinact_module := "github.com/suzuki-shunsuke/pinact/v4/cmd/pinact@v" + pinact_version llvm_version := "22" zizmor_version := "1.26.1" primary_binary := if os_family() == "windows" { "out/build/reference/src/cdt.exe" } else { "out/build/reference/src/cdt" } +rng_benchmark_binary := if os_family() == "windows" { "out/build/reference/tests/CDT_rng_benchmark.exe" } else { "out/build/reference/tests/CDT_rng_benchmark" } # Build the supported configuration through the repository build script. [group('workflows')] build: - {{ if os_family() == "windows" { "scripts\\build.bat" } else { "just _build-unix" } }} + {{ if os_family() == "windows" { "cmd.exe //d //c scripts/build.bat" } else { "just _build-unix" } }} # Run fast, non-mutating local validation. [group('workflows')] -check: _justfile-check _format-check _yaml-check _action-lint _zizmor _whitespace-check _cmake-check python-check +check: _justfile-check _format-check _yaml-check _action-lint _zizmor _whitespace-check _cmake-check release-check python-check semgrep semgrep-test @echo "Checks complete." # Run the comprehensive pre-commit/pre-push validation gate. @@ -26,6 +28,11 @@ check: _justfile-check _format-check _yaml-check _action-lint _zizmor _whitespac ci: check _pinact-check build @echo "CI validation complete." +# Measure run-owned PCG sampling against the removed entropy-per-draw design. +[group('workflows')] +benchmark-rng draws='10000': build + {{ rng_benchmark_binary }} {{ draws }} + # Apply safe automatic formatting to C++/Python source and the Justfile. [group('workflows')] fix: _format-fix python-fix @@ -37,6 +44,46 @@ fix: _format-fix python-fix clang-tidy: ./scripts/clang-tidy.sh +# Validate release metadata, citation fields, and version synchronization. +[group('workflows')] +release-check: _ensure-uv + uv run --locked python scripts/release_check.py + +# Scan production and correctness-test sources for repository-owned policies. +[group('workflows')] +semgrep: _ensure-uv + #!/usr/bin/env bash + set -euo pipefail + state_dir="$(mktemp -d "${TMPDIR:-/tmp}/cdt-semgrep-state.XXXXXX")" + trap 'rm -rf "$state_dir"' EXIT + SEMGREP_LOG_FILE="$state_dir/semgrep.log" SEMGREP_SEND_METRICS=off \ + SEMGREP_SETTINGS_FILE="$state_dir/settings.yml" SEMGREP_VERSION_CACHE_PATH="$state_dir/version-cache" \ + uv run --locked semgrep scan --error --strict --timeout 120 --no-git-ignore \ + --config semgrep.yaml --exclude tests/semgrep include src tests + +# Test repository-owned Semgrep rules against annotated positive and negative fixtures. +[group('workflows')] +semgrep-test: _ensure-uv + #!/usr/bin/env bash + set -euo pipefail + config_dir="$(mktemp -d "${TMPDIR:-/tmp}/cdt-semgrep-config.XXXXXX")" + state_root="$(mktemp -d "${TMPDIR:-/tmp}/cdt-semgrep-state.XXXXXX")" + cleanup() { + rm -rf "$config_dir" "$state_root" + } + trap cleanup EXIT + + while IFS= read -r -d '' fixture; do + rel="${fixture#tests/semgrep/}" + config_path="$config_dir/${rel%.*}.yaml" + state_dir="$state_root/${rel%.*}" + mkdir -p "$(dirname "$config_path")" "$state_dir" + uv run --locked python scripts/semgrep_fixture_config.py "$fixture" "$PWD/semgrep.yaml" "$config_path" + SEMGREP_LOG_FILE="$state_dir/semgrep.log" SEMGREP_SEND_METRICS=off \ + SEMGREP_SETTINGS_FILE="$state_dir/settings.yml" SEMGREP_VERSION_CACHE_PATH="$state_dir/version-cache" \ + uv run --locked semgrep scan --test --strict --config "$config_path" "$fixture" + done < <(find tests/semgrep -type f ! -name '*.fixed' -print0) + # Build and exercise one supported Linux sanitizer configuration. [group('workflows')] sanitize kind: @@ -44,24 +91,35 @@ sanitize kind: # Run every non-mutating Python source check. [group('workflows')] -python-check: python-format-check python-lint python-typecheck +python-check: python-format-check python-lint python-typecheck python-support-test python-entrypoint-test @echo "Python source checks complete." # Apply Ruff lint fixes and formatting to Python source. [group('workflows')] python-fix: _ensure-uv - uv run --locked ruff check src/ --fix - uv run --locked ruff format src/ + uv run --locked ruff check scripts/ --fix + uv run --locked ruff format scripts/ # Check Python formatting with Ruff. [group('workflows')] python-format-check: _ensure-uv - uv run --locked ruff format --check src/ + uv run --locked ruff format --check scripts/ # Lint Python source with Ruff. [group('workflows')] python-lint: _ensure-uv - uv run --locked ruff check src/ + uv run --locked ruff check scripts/ + +# Test repository-owned Python support scripts. +[group('workflows')] +python-support-test: _ensure-uv + uv run --locked python -m unittest discover -s scripts/tests -p 'test_*.py' + +# Smoke-test installed entry points without loading optional experiment dependencies. +[group('workflows')] +python-entrypoint-test: _ensure-uv + uv run --locked cdt-optimize-initialize --help >/dev/null + uv run --locked cdt-mnist-experiment --help >/dev/null # Synchronize the lightweight Python development environment from the lockfile. [group('workflows')] @@ -76,7 +134,7 @@ python-sync-experiments: _ensure-uv # Type-check Python support code with ty. [group('workflows')] python-typecheck: _ensure-uv - uv run --locked ty check src/ --error all + uv run --locked ty check scripts/*.py scripts/tests/*.py --error all # Build as needed and run the primary CDT++ executable. [group('workflows')] @@ -97,21 +155,14 @@ default: @just --list [private] -_action-lint: +_action-lint: _ensure-uv #!/usr/bin/env bash set -euo pipefail files=() while IFS= read -r -d '' file; do [[ -f "$file" ]] && files+=("$file") done < <(git ls-files -co --exclude-standard -z -- '.github/workflows/*.yml' '.github/workflows/*.yaml') - if command -v actionlint >/dev/null; then - actionlint "${files[@]}" - elif command -v pkgx >/dev/null; then - pkgx actionlint "${files[@]}" - else - echo "actionlint is required; install it or install pkgx." >&2 - exit 1 - fi + uv run --locked actionlint "${files[@]}" [private] _build-unix: @@ -147,43 +198,29 @@ _ensure-uv: fi [private] -_resolve-clang-format: - #!/usr/bin/env bash - set -euo pipefail - clang_format="$(command -v clang-format-{{ llvm_version }} || command -v clang-format || true)" - if [[ -n "$clang_format" ]] && ! "$clang_format" --version | grep -Eq 'clang-format version {{ llvm_version }}([.]|$)'; then - clang_format="" - fi - if [[ -z "$clang_format" ]] && command -v pkgx >/dev/null; then - exec pkgx +llvm.org@{{ llvm_version }} -- just _resolve-clang-format - fi - [[ -n "$clang_format" ]] || { echo "clang-format {{ llvm_version }} is required; install it or install pkgx." >&2; exit 1; } - printf '%s\n' "$clang_format" - -[private] -_format-check: +_format-check: _ensure-uv #!/usr/bin/env bash set -euo pipefail - clang_format="$(just _resolve-clang-format)" + uv run --locked clang-format --version | grep -Eq 'clang-format version {{ llvm_version }}([.]|$)' files=() while IFS= read -r -d '' file; do [[ -f "$file" ]] && files+=("$file") done < <(git ls-files -co --exclude-standard -z -- '*.c' '*.cc' '*.cpp' '*.h' '*.hpp') if [[ "${#files[@]}" -gt 0 ]]; then - "$clang_format" --dry-run --Werror "${files[@]}" + uv run --locked clang-format --dry-run --Werror "${files[@]}" fi [private] -_format-fix: +_format-fix: _ensure-uv #!/usr/bin/env bash set -euo pipefail - clang_format="$(just _resolve-clang-format)" + uv run --locked clang-format --version | grep -Eq 'clang-format version {{ llvm_version }}([.]|$)' files=() while IFS= read -r -d '' file; do [[ -f "$file" ]] && files+=("$file") done < <(git ls-files -co --exclude-standard -z -- '*.c' '*.cc' '*.cpp' '*.h' '*.hpp') if [[ "${#files[@]}" -gt 0 ]]; then - "$clang_format" -i "${files[@]}" + uv run --locked clang-format -i "${files[@]}" fi [private] @@ -203,7 +240,10 @@ _pinact *args: if command -v pkgx >/dev/null; then exec pkgx go run "{{ pinact_module }}" {{ args }} fi - echo "pinact {{ pinact_version }} is required; install it with Homebrew or install pkgx." >&2 + if command -v go >/dev/null; then + exec go run "{{ pinact_module }}" {{ args }} + fi + echo "pinact {{ pinact_version }} is required; install it, Go, or pkgx." >&2 exit 1 [private] @@ -225,21 +265,14 @@ _whitespace-check: [[ "$status" -eq 1 ]] || exit "$status" [private] -_yaml-check: +_yaml-check: _ensure-uv #!/usr/bin/env bash set -euo pipefail files=(.clang-format) while IFS= read -r -d '' file; do [[ -f "$file" ]] && files+=("$file") done < <(git ls-files -co --exclude-standard -z -- '*.yml' '*.yaml') - if command -v yamllint >/dev/null; then - yamllint "${files[@]}" - elif command -v pkgx >/dev/null; then - pkgx yamllint "${files[@]}" - else - echo "yamllint is required; install it or install pkgx." >&2 - exit 1 - fi + uv run --locked yamllint "${files[@]}" [private] _zizmor: diff --git a/README.md b/README.md index f6ee23c32f..d3583039ae 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,8 @@ ## Maintenance status -CDT++ is being prepared for one final C++23 release, v1.0.0, after which this repository will be archived. It is +CDT++ v1.0.0-rc1 is the release candidate for the final C++23 release, v1.0.0, after which this repository will be +archived. It is maintained as an independent scientific reference and regression oracle for [causal-triangulations](https://github.com/acgetchell/causal-triangulations), the supported Rust successor. New C++ work is limited to correctness, reproducibility, cross-implementation validation, the complete supported 2+1D move @@ -35,6 +36,7 @@ set, and the final release contract tracked by [issue #90](https://github.com/ac - [Run](#run) - [Usage](#usage) - [Documentation](#documentation) + - [Citing CDT++](#citing-cdt) - [Testing](#testing) - [Static Analysis](#static-analysis) - [Sanitizers](#sanitizers) @@ -137,17 +139,15 @@ Windows development. ### Current reference-suite status -With the pinned baseline, the reference configuration and build succeed on macOS with AppleClang. `build.sh` runs -eleven supported smoke tests on Unix: nine lightweight CLI integration tests and two focused doctest suites. The known -failing `initialize` scenario is excluded from Windows smoke runs, while the other initialization cases remain enabled. -The complete registered suite additionally runs the full unit-test executable and a focused utilities registration; -the focused registrations remain available for quick iteration and are labeled as full-suite duplicates for sanitizer -runs. +With the pinned baseline, the reference configuration and build succeed on macOS with AppleClang. `build.sh` runs all +21 CTest entries on every supported platform: one unit-test launcher containing 83 doctest scenarios and 20 CLI +integration tests. The same `reference-smoke` preset is the supported local and CI contract; there are no overlapping +focused registrations that can pass while omitting another doctest suite. ## Setup This project uses [CMake]+[Ninja] to build C++23 sources and [vcpkg] manifest mode to manage C++ libraries. macOS with -AppleClang is the primary v1.0.0 restoration target; the remaining compiler and platform matrix will be recorded as +AppleClang is the primary v1.0.0-rc1 validation target; the remaining compiler and platform matrix will be recorded as it is verified. ### Prerequisites @@ -190,25 +190,30 @@ just sanitize asan # Build and exercise one Linux sanitizer preset just build # Bootstrap, configure, build, and smoke-test just run --help # Build as needed and run cdt with forwarded arguments just ci # Comprehensive pre-commit/pre-push validation +just release-check # Validate release metadata and citation fields just update-actions # Update and repin Actions with pinact, then validate -just python-sync # Install the locked Ruff and ty development environment +just python-sync # Install the locked Python development environment just python-check # Check Python formatting, lint, and types just python-fix # Apply safe Ruff fixes and formatting ``` -`check` covers repository-wide C++ formatting, Python formatting/lint/type checks, YAML, GitHub Actions syntax and -security, whitespace, and CMake preset parsing. `ci` adds the pinact policy check and the supported build/test -contract. Install the developer tools with -Homebrew, use equivalent system packages, or let pkgx supply them ephemerally; pkgx remains optional. For example: +`check` covers repository-wide C++ formatting, Python formatting/lint/type checks, release metadata and citation +fields, YAML, GitHub Actions syntax and security, whitespace, and CMake preset parsing. `ci` adds the pinact policy +check and the supported build/test contract. The GitHub Actions Ubuntu GCC, Ubuntu Clang, macOS AppleClang, and +Windows MSVC jobs all run the same `just ci` command. Windows continues to compile with native MSVC; the locked +Python environment supplies `clang-format` only as a source formatter. Install the developer tools with Homebrew, +use equivalent system packages, or let pkgx supply the Unix environment ephemerally; pkgx remains optional. For +example: ```bash -pkgx +just.systems +git-scm.org +cmake.org +ninja-build.org +python.org +llvm.org@22 \ - +yamllint +actionlint +zizmor just check +uv sync --locked --group dev +pkgx +just.systems@1.57.0 +git-scm.org +cmake.org +ninja-build.org +python.org +zizmor just check ``` [pinact](https://github.com/suzuki-shunsuke/pinact) uses [`.pinact.yaml`](.pinact.yaml) to retain immutable action -SHAs, readable release comments, and a seven-day release cooldown. `just update-actions` uses an installed pinact or -a pkgx-provided Go fallback, then requires `yamllint`, `actionlint`, and `zizmor` to pass. +SHAs, readable release comments, and a seven-day release cooldown. `just update-actions` uses an installed pinact, +Go, or a pkgx-provided Go fallback, then requires `yamllint`, `actionlint`, and `zizmor` to pass. The locked uv +development environment provides `clang-format`, `yamllint`, and `actionlint` consistently on every platform. ### vcpkg maintenance @@ -298,6 +303,7 @@ Usage:./cdt (--spherical | --toroidal) -n SIMPLICES -t TIMESLICES [--init INITIAL RADIUS] [--foliate FOLIATION SPACING] [--no-output] + [--seed SEED] -k K --alpha ALPHA --lambda LAMBDA @@ -308,7 +314,7 @@ Optional arguments are in square brackets. Examples: ./cdt --spherical -n 32000 -t 11 --alpha 0.6 -k 1.1 --lambda 0.1 --passes 1000 -./cdt -s -n32000 -t11 -a.6 -k1.1 -l.1 -p1000 +./cdt -s -n32000 -t11 -a.6 -k1.1 -l.1 -p1000 --seed 92 Options: -h [ --help ] Show this message @@ -322,6 +328,8 @@ Options: -f [ --foliate ] arg (=1) Foliation spacing --no-output Do not write checkpoint or final triangulation files + --seed arg Root random seed (default: operating-system + entropy) -a [ --alpha ] arg Negative squared geodesic length of 1-d timelike edges -k [ --k ] arg K = 1/(8*pi*G_newton) @@ -342,6 +350,14 @@ links (in 2+1 spacetime), and the timelike faces (in 3+1 spacetime). Online documentation is at . +The scientific transition, proposal-ratio, geometry-delta, counter, and +precision contracts are recorded in +[`docs/metropolis-hastings.md`](docs/metropolis-hastings.md). +Seed replay, PCG stream ownership, checkpoint metadata, and the parallel stream +policy are recorded in [`docs/reproducibility.md`](docs/reproducibility.md). +The repository-wide scientific bibliography is maintained in +[`REFERENCES.md`](REFERENCES.md). + If you have [Doxygen] installed you can generate the same information locally using the configuration file in `docs/Doxyfile` by simply typing at the top level directory ([Doxygen] will recursively search): @@ -358,12 +374,25 @@ various graphs to be autogenerated by [Doxygen] using [GraphViz]. If you do not have GraphViz installed, set this option to **NO** (along with `UML_LOOK`). +## Citing CDT++ + +If CDT++ contributes to published work, cite the software using +[`CITATION.cff`](CITATION.cff) and cite the scientific methods relevant to the +work from [`REFERENCES.md`](REFERENCES.md). The software citation records the +current declared release, `1.0.0-rc1`. Advance its version and release date +together with the CMake, vcpkg, Python-tooling, Doxygen, and CLI metadata for +subsequent releases. + ## Testing -Run `just build`; it delegates to `./scripts/build.sh`, builds the test target, and executes the 22 supported smoke tests. -These include two focused doctest runs for the Boost.Compat `function_ref` migration and causal-foliation construction, -plus 20 executable integration tests covering normal CLI use and invalid-boundary rejection. Run `just ci` for the -complete local validation gate. +Run `just build`; it delegates to `./scripts/build.sh`, builds the test target, and executes all 21 CTest entries: one +unit-test launcher containing 83 doctest scenarios plus 20 executable integration tests covering normal CLI use and +invalid-boundary rejection. CTest labels the launcher `unit` and every process-level test `integration`; invalid-input +tests also carry the `cli-boundary` subcategory. Run `just ci` for the complete local validation gate. + +`just check` also runs the repository-owned Semgrep policy and its annotated +fixtures. Use `just semgrep-test` while changing the rules and `just semgrep` to +scan the real source tree for false positives. The doctest executable can also be run directly: @@ -371,17 +400,17 @@ The doctest executable can also be run directly: ./out/build/reference/tests/CDT_unit_tests ``` -To rerun the supported smoke suite without rebuilding: +To rerun the complete suite without rebuilding: ```bash ctest --preset reference-smoke ``` -To run every currently registered test, including the full unit-test executable and focused duplicate registrations, -bypass the smoke filter explicitly: +To run a specific test category, use: ```bash -ctest --test-dir out/build/reference --output-on-failure +ctest --preset reference-smoke -L unit +ctest --preset reference-smoke -L integration ``` In addition to the command line output, you can see detailed results in the @@ -411,16 +440,19 @@ remains experimental because third-party dependencies are not instrumented. ## Optimizing Parameters -[CometML] is used to record [Experiments] conducted by `src/optimize-initialize.py`; `src/test.py` is the existing -TensorFlow MNIST experiment. These optional, heavyweight dependencies are kept out of the normal lint environment. +[CometML] is used to record [Experiments] conducted by the `cdt-optimize-initialize` command; +`cdt-mnist-experiment` runs the existing TensorFlow MNIST experiment. Both commands are registered in +`pyproject.toml`, while their optional, heavyweight dependencies remain outside the normal development environment. Synchronize them from the same uv lockfile when working on the experiment scripts: ```bash just python-sync-experiments -uv run --locked --group experiments python src/optimize-initialize.py +uv run --locked --group experiments cdt-optimize-initialize +uv run --locked --group experiments cdt-mnist-experiment ``` -Set `COMET_API_KEY` before starting an online experiment. The experiment results are then available in Comet. +Run these commands from the repository root. Set `COMET_API_KEY` before starting the parameter optimization; use +`--repository-root` when invoking it from another directory. The experiment results are then available in Comet. Migration of these legacy scripts to Python 3.14, PyTorch, and the current Comet API is tracked by [#104](https://github.com/acgetchell/CDT-plusplus/issues/104). @@ -443,7 +475,7 @@ Your code should pass Continuous Integration: - `just check` for fast, non-mutating source, YAML, workflow, and CMake validation -- `just ci` for the supported build and smoke-test contract before pushing +- `just ci` for the supported build and complete validation contract before pushing The slower sanitizer workflows remain available through [GitHub Actions] and repository commands when relevant to a change: @@ -467,8 +499,8 @@ Optional: [#45336]: https://github.com/microsoft/vcpkg/issues/45336 [#40623]: https://github.com/microsoft/vcpkg/issues/40623 [#23637]: https://github.com/microsoft/vcpkg/issues/23637 -[CDT]: https://arxiv.org/abs/hep-th/0105267 -[CGAL]: https://www.cgal.org +[CDT]: REFERENCES.md#cdt-framework-2001 +[CGAL]: REFERENCES.md#cgal-triangulations [CMake]: https://www.cmake.org [doctest]: https://github.com/doctest/doctest [guidelines]: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines @@ -495,7 +527,7 @@ Optional: [vcpkg]: https://github.com/Microsoft/vcpkg [C++]: https://isocpp.org/ [Pitchfork Layout]: https://api.csswg.org/bikeshed/?force=1&url=https://raw.githubusercontent.com/vector-of-bool/pitchfork/develop/data/spec.bs#tld.docs -[PCG]: http://www.pcg-random.org/paper.html +[PCG]: REFERENCES.md#pcg-random-number-generators [TestU01]: http://simul.iro.umontreal.ca/testu01/tu01.html [CONTRIBUTING.md]: https://github.com/acgetchell/CDT-plusplus/blob/main/.github/CONTRIBUTING.md [CODE_OF_CONDUCT.md]: https://github.com/acgetchell/CDT-plusplus/blob/main/.github/CODE_OF_CONDUCT.md diff --git a/REFERENCES.md b/REFERENCES.md new file mode 100644 index 0000000000..38692ac4ba --- /dev/null +++ b/REFERENCES.md @@ -0,0 +1,73 @@ +# References and Citations + +## How to Cite This Software + +If CDT++ contributes to research or a project, cite it using the structured +metadata in [`CITATION.cff`](CITATION.cff). GitHub and other citation tools can +generate BibTeX, APA, and additional formats from that file. + +Quick citation for the current declared release: + +```text +Adam Getchell. 2026. CDT-plusplus: Causal Dynamical Triangulations in C++. +Version 1.0.0-rc1. GitHub. https://github.com/acgetchell/CDT-plusplus +``` + +The sections below are grouped by scientific domain. Cite the entries relevant +to the algorithms or results used in a given work. Source documentation links +directly to the applicable entry so that provenance stays close to the +implementation while complete metadata remains centralized here. + +## Foundational Causal Dynamical Triangulations Theory + +### Original CDT framework + +#### CDT framework (2001) + +J. Ambjørn, J. Jurkiewicz, and R. Loll, “Dynamically triangulating Lorentzian +quantum gravity,” *Nuclear Physics B* 610, no. 1–2 (2001), 347–382. +DOI: [10.1016/S0550-3213(01)00297-8]() + +#### Three-dimensional CDT (2001) + +J. Ambjørn, J. Jurkiewicz, and R. Loll, “Nonperturbative 3D Lorentzian quantum +gravity,” *Physical Review D* 64, no. 4 (2001), 044011. +DOI: [10.1103/PhysRevD.64.044011](https://doi.org/10.1103/PhysRevD.64.044011) + +## Monte Carlo Methods + +### Metropolis-Hastings algorithm + +W. K. Hastings, “Monte Carlo sampling methods using Markov chains and their +applications,” *Biometrika* 57, no. 1 (1970), 97–109. +DOI: [10.1093/biomet/57.1.97](https://doi.org/10.1093/biomet/57.1.97) + +## Regge Calculus and Discrete Action + +### Regge calculus + +T. Regge, “General relativity without coordinates,” *Il Nuovo Cimento* 19, +no. 3 (1961), 558–571. +DOI: [10.1007/BF02733251](https://doi.org/10.1007/BF02733251) + +## Simplicial Topology and Local Moves + +### Pachner moves + +U. Pachner, “P.L. homeomorphic manifolds are equivalent by elementary +shellings,” *European Journal of Combinatorics* 12, no. 2 (1991), 129–145. +DOI: [10.1016/S0195-6698(13)80080-7]() + +## Computational Geometry and Random-Number Generation + +### CGAL triangulations + +The CGAL Project, *CGAL User and Reference Manual*. CGAL Editorial Board, 6.2 +edition (2026). + +### PCG random-number generators + +M. E. O’Neill, “PCG: A family of simple fast space-efficient statistically good +algorithms for random number generation,” Harvey Mudd College Computer Science +Department technical report HMC-CS-2014-0905 (2014). + diff --git a/cmake/RunCdtNoOutputTest.cmake b/cmake/RunCdtNoOutputTest.cmake index 7160bbc690..688c333233 100644 --- a/cmake/RunCdtNoOutputTest.cmake +++ b/cmake/RunCdtNoOutputTest.cmake @@ -14,15 +14,15 @@ execute_process( if(NOT help_result EQUAL 0) message(FATAL_ERROR "cdt --help failed:\n${help_output}\n${help_error}") endif() -if(NOT help_output MATCHES "--no-output") - message(FATAL_ERROR "cdt --help does not document --no-output") +if(NOT help_output MATCHES "--no-output" OR NOT help_output MATCHES "--seed") + message(FATAL_ERROR "cdt --help does not document --no-output and --seed") endif() file(REMOVE_RECURSE "${TEST_DIRECTORY}") file(MAKE_DIRECTORY "${TEST_DIRECTORY}") execute_process( - COMMAND "${CDT_EXECUTABLE}" -s -n64 -t3 -a0.6 -k1.1 -l0.1 -p1 -c1 --no-output + COMMAND "${CDT_EXECUTABLE}" -s -n64 -t3 -a0.6 -k1.1 -l0.1 -p1 -c1 --no-output --seed 92 WORKING_DIRECTORY "${TEST_DIRECTORY}" RESULT_VARIABLE run_result OUTPUT_VARIABLE run_output @@ -33,6 +33,9 @@ endif() if(run_output MATCHES "Writing to file" OR run_error MATCHES "Writing to file") message(FATAL_ERROR "cdt --no-output attempted to write a triangulation file") endif() +if(NOT run_output MATCHES "Effective random seed: 92") + message(FATAL_ERROR "cdt did not report the requested effective seed:\n${run_output}") +endif() file(GLOB_RECURSE generated_files LIST_DIRECTORIES false "${TEST_DIRECTORY}/*") if(generated_files) diff --git a/cmake/Version.hpp.in b/cmake/Version.hpp.in new file mode 100644 index 0000000000..027b9a9adb --- /dev/null +++ b/cmake/Version.hpp.in @@ -0,0 +1,17 @@ +/******************************************************************************* + Causal Dynamical Triangulations in C++ using CGAL + + Copyright © 2013–2026 Adam Getchell + ******************************************************************************/ + +#ifndef CDT_VERSION_HPP +#define CDT_VERSION_HPP + +#include + +namespace cdt +{ +inline constexpr std::string_view VERSION{"@CDT_VERSION@"}; +} // namespace cdt + +#endif // CDT_VERSION_HPP diff --git a/docs/Doxyfile b/docs/Doxyfile index aac422d285..c004f31353 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -38,7 +38,7 @@ PROJECT_NAME = "CDT++" # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = +PROJECT_NUMBER = 1.0.0-rc1 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/docs/metropolis-hastings.md b/docs/metropolis-hastings.md new file mode 100644 index 0000000000..0ddd201aed --- /dev/null +++ b/docs/metropolis-hastings.md @@ -0,0 +1,107 @@ +# Metropolis-Hastings transition contract + +CDT++ samples a triangulation `T` with target weight proportional to `exp(-S(T))`. +For a proposed triangulation `T'`, the implemented acceptance probability is + +```text +min(1, exp(S(T) - S(T')) * q(T | T') / q(T' | T)). +``` + +This is the Metropolis-Hastings rule of Hastings +[Hastings1970](../REFERENCES.md#metropolis-hastings-algorithm). The +three-dimensional causal triangulations, Regge action, and local move set +follow Ambjørn, Jurkiewicz, and Loll +[AmbjornJurkiewiczLoll2001-3D](../REFERENCES.md#three-dimensional-cdt-2001). + +## Proposal kernel + +Each transition first chooses one of the five 3D move types uniformly. It then +chooses one raw site uniformly from the move-specific domain below. The move is +attempted only at that site. An inapplicable site, failed construction, or +invalid geometry delta is an explicit rejected self-transition. + +| Move | Raw proposal sites in `T` | Reverse move | +| --- | --- | --- | +| `(2,3)` | `N3_22(T)` two-two cells | `(3,2)` | +| `(3,2)` | `N1_TL(T)` timelike edges | `(2,3)` | +| `(2,6)` | `N3_13(T)` one-three cells | `(6,2)` | +| `(6,2)` | `N0(T)` vertices | `(2,6)` | +| `(4,4)` | `N1_SL(T)` spacelike edges | `(4,4)` | + +For a raw-site count `C_m(T)`, a particular proposal has probability + +```text +q(T' | T) = 1 / (5 * C_m(T)). +``` + +The factor `1/5` cancels between a move and its reverse, giving + +```text +q(T | T') / q(T' | T) = C_m(T) / C_reverse(m)(T'). +``` + +The site definitions give a unique inverse site for each successful local +retriangulation: a `(2,3)` face becomes the timelike edge used by `(3,2)`; a +successful `(2,6)` move creates the vertex used by `(6,2)`; and a `(4,4)` pivot +edge becomes the opposite pivot edge. Each `(2,6)` proposal examines exactly +one uniformly selected `(1,3)` cell. Failed raw sites are not silently skipped, +because conditioning on only the movable subset would change `q` and invalidate +the count ratio above. + +Move-type selection, raw-site selection, internal candidate-construction +ordering, and the acceptance draw all consume the same run-owned random engine. +Given the same starting manifold, an explicit seed therefore replays the +complete transition sequence, including the technical edge ordering used to +construct a `(6,2)` candidate. +Initialization uses a separate named stream derived from the same root seed, so +changes in point-generation draw counts do not shift the transition sequence. +The CLI, checkpoint metadata, stream ownership, and future parallel policy are +documented in [Reproducible random runs](reproducibility.md). + +## State and geometry deltas + +Candidates are constructed off to the side and checked against the exact +topology delta before the acceptance calculation. Only an accepted candidate +replaces the canonical manifold. Thus every decision uses the state committed +by the immediately preceding transition. + +| Move | `ΔN3` | `ΔN3_31_13` | `ΔN3_22` | `ΔN1_TL` | +| --- | ---: | ---: | ---: | ---: | +| `(2,3)` | `+1` | `0` | `+1` | `+1` | +| `(3,2)` | `-1` | `0` | `-1` | `-1` | +| `(2,6)` | `+4` | `+4` | `0` | `+2` | +| `(6,2)` | `-4` | `-4` | `0` | `-2` | +| `(4,4)` | `0` | `0` | `0` | `0` | + +In particular, `(6,2)` removes two `(3,1)` and two `(1,3)` simplices while +leaving `N3_22` unchanged. The runtime invariant check also covers `N2`, total +and classified edges, spacelike edges, vertices, validity, and foliation bounds. + +## Counters + +The counters obey two identities: + +```text +proposed = accepted + rejected +attempted = succeeded + failed = proposed +``` + +`succeeded` means that candidate construction produced a valid manifold. Such a +candidate can still be rejected by Metropolis-Hastings. `failed` means the raw +site was inapplicable or candidate construction violated an invariant; every +failure is therefore also a rejection. + +## Numerical policy + +The action, action difference, exponential, proposal ratio, and acceptance +probability remain MPFR values at 256-bit precision. Every MPFR operation uses +round-to-nearest with ties to an even significand (`MPFR_RNDN`). Conversion to +`long double` occurs only for diagnostic output; an acceptance draw is compared +directly with the MPFR probability. + +## References + +Bibliographic metadata for +[Hastings1970](../REFERENCES.md#metropolis-hastings-algorithm) and +[AmbjornJurkiewiczLoll2001-3D](../REFERENCES.md#three-dimensional-cdt-2001) is +maintained in the repository-wide [`REFERENCES.md`](../REFERENCES.md). diff --git a/docs/reproducibility.md b/docs/reproducibility.md new file mode 100644 index 0000000000..581026222f --- /dev/null +++ b/docs/reproducibility.md @@ -0,0 +1,60 @@ +# Reproducible random runs + +CDT++ owns random state at the simulation boundary. The root `cdt::Random` +engine records a 64-bit seed and derives named PCG streams for independent +subsystems: + +- stream `0` generates the initial triangulation; +- stream `1` selects and constructs Metropolis-Hastings transitions. + +Pass `--seed SEED` to `cdt` or `initialize` to replay the random inputs to a +run. Without that option, the command obtains operating-system entropy once +and prints the effective seed. Checkpoint and final OFF filenames also include +`seed-SEED`; simulation checkpoints include `pass-PASS`. The seed is metadata +in the filename rather than extra OFF content so files remain readable by +standard CGAL triangulation parsers. + +```console +./out/build/reference/src/cdt -s -n640 -t4 -a0.6 -k1.1 -l0.1 -p10 --seed 92 +./out/build/reference/src/initialize -s -n640 -t4 --seed 92 +``` + +Distributions are short-lived operations applied to a caller-owned engine. +Algorithms do not acquire entropy per sample, and tests use fixed seeds. The +repository-owned Semgrep rules prevent new direct `std::random_device` or PCG +engine construction outside `Random.hpp`. The supported spherical CGAL point +generator receives a single seed derived from its caller-owned initialization +stream rather than using CGAL's hidden default generator. + +The reproducibility guarantee is exact for PCG draws and generated +initialization points. Given the same starting manifold, the complete +Metropolis transition sequence and counters also replay exactly. A freshly +constructed triangulation is not promised to have byte-identical topology +across CGAL versions, platforms, or builds: points on the same spherical layer +are cospherical, so CGAL may choose a different valid tetrahedralization when +resolving geometric ties. + +## Parallel stream policy + +`cdt::Random` is not internally synchronized. One mutable engine belongs to +one sequential run or one thread. Before parallel stochastic work is enabled, +each worker must receive `root.split(worker_stream)` with a unique, stable +stream identifier; engines must not be shared concurrently. PCG's stream +selector gives independently parameterized sequences while retaining the root +seed needed for replay. See the repository's +[PCG reference](../REFERENCES.md#pcg-random-number-generators). + +## Move-selection performance check + +Issue #105 removes the old entropy-per-draw behavior from move-heavy paths. +The benchmark retains that behavior only as a labeled baseline and compares it +with one run-owned PCG stream: + +```console +just benchmark-rng 10000 +``` + +The diagnostic reports both durations and their ratio. It is intentionally not +a pass/fail CI test because operating-system entropy latency and runner load are +machine-dependent; replay and distribution boundaries remain correctness tests +in `just ci`. diff --git a/include/Apply_move.hpp b/include/Apply_move.hpp index ce0ead27f4..c8fa7a3c8b 100644 --- a/include/Apply_move.hpp +++ b/include/Apply_move.hpp @@ -11,12 +11,7 @@ #ifndef CDT_PLUSPLUS_APPLY_MOVE_HPP #define CDT_PLUSPLUS_APPLY_MOVE_HPP -#include - -#include -#include #include -#include #include /** @@ -26,23 +21,14 @@ * \tparam FunctionType The type of move applied to the manifold * \param t_manifold The manifold on which to make the Pachner move * \param t_move The Pachner move + * \param arguments Explicit dependencies forwarded to the move * \return The expected or unexpected result in a std::expected */ -template -auto constexpr apply_move(ManifoldType const& t_manifold, FunctionType t_move) - -> decltype(auto) +template +auto constexpr apply_move(ManifoldType const& t_manifold, FunctionType t_move, + Arguments&&... arguments) -> decltype(auto) { - if (auto result = std::invoke(t_move, t_manifold); result.has_value()) - { - return result; - } - else // NOLINT - { - // Log errors - spdlog::debug("apply_move called.\n"); - spdlog::debug("{}", result.error()); - return result; - } + return std::invoke(t_move, t_manifold, std::forward(arguments)...); } #endif // CDT_PLUSPLUS_APPLY_MOVE_HPP diff --git a/include/Ergodic_moves_3.hpp b/include/Ergodic_moves_3.hpp index 72a91a565f..986756977f 100644 --- a/include/Ergodic_moves_3.hpp +++ b/include/Ergodic_moves_3.hpp @@ -11,13 +11,18 @@ /// The helper functions for the moves operate on the level of the /// Delaunay_Triangulation_3. /// C++23 support is required for std::expected. +/// @see [Pachner moves](../REFERENCES.md#pachner-moves) +/// @see [Three-dimensional CDT move +/// set](../REFERENCES.md#three-dimensional-cdt-2001) #ifndef CDT_PLUSPLUS_ERGODIC_MOVES_3_HPP #define CDT_PLUSPLUS_ERGODIC_MOVES_3_HPP +#include + #include +#include -#include "Formatters.hpp" #include "Manifold.hpp" #include "Move_tracker.hpp" @@ -59,6 +64,18 @@ namespace ergodic_moves return edge.first != nullptr && valid_index(edge.second) && valid_index(edge.third) && edge.second != edge.third; } + + /// Select exactly one raw proposal site uniformly. + template + [[nodiscard]] inline auto random_element(Container const& candidates, + Generator& generator) + -> std::optional + { + if (candidates.empty()) { return std::nullopt; } + std::uniform_int_distribution distribution{ + 0, candidates.size() - 1}; + return candidates[distribution(generator)]; + } } // namespace detail /// @brief Perform a null move @@ -99,15 +116,9 @@ namespace ergodic_moves if (triangulation.flip(to_be_moved, i)) { -#ifndef NDEBUG - spdlog::trace("Facet {} was flippable.\n", i); -#endif flipped = true; break; } -#ifndef NDEBUG - spdlog::trace("Facet {} was not flippable.\n", i); -#endif } return flipped; } // try_23_move @@ -124,18 +135,16 @@ namespace ergodic_moves /// /// @param t_manifold The simplicial manifold /// @returns The Expected (2,3) moved manifold or an Unexpected - [[nodiscard]] inline auto do_23_move(Manifold const& t_manifold) -> Expected + template + [[nodiscard]] inline auto do_23_move(Manifold const& t_manifold, + Generator& generator) -> Expected { -#ifndef NDEBUG - spdlog::debug("{} called.\n", __PRETTY_FUNCTION__); -#endif - Delaunay triangulation{t_manifold.delaunay_snapshot()}; auto two_two = foliated_triangulations::filter_cells<3>( foliated_triangulations::collect_cells<3>(triangulation), Cell_type::TWO_TWO); // Shuffle the container to create a random sequence of (2,2) cells - std::ranges::shuffle(two_two, utilities::make_random_generator()); + std::ranges::shuffle(two_two, generator); // Try a (2,3) move on successive cells in the sequence if (std::ranges::any_of(two_two, [&](auto& cell) { return try_23_move(triangulation, cell); @@ -145,10 +154,29 @@ namespace ergodic_moves } // We've run out of (2,2) cells std::string const msg = "No (2,3) move possible.\n"; - spdlog::warn(msg); return std::unexpected(msg); } + /// @brief Propose one (2,3) site for Metropolis-Hastings. + /// @details Unlike do_23_move(), this samples exactly one of the N3(2,2) + /// cells. An inapplicable selected cell is a rejected proposal rather than a + /// reason to condition the proposal distribution on the movable subset. + template + [[nodiscard]] inline auto propose_23_move(Manifold const& t_manifold, + Generator& generator) -> Expected + { + auto triangulation = t_manifold.delaunay_snapshot(); + auto two_two = foliated_triangulations::filter_cells<3>( + foliated_triangulations::collect_cells<3>(triangulation), + Cell_type::TWO_TWO); + auto const candidate = detail::random_element(two_two, generator); + if (candidate && try_23_move(triangulation, *candidate)) + { + return detail::make_manifold(std::move(triangulation), t_manifold); + } + return std::unexpected("Selected (2,3) proposal site is not movable.\n"); + } + /// @brief Perform a TriangulationDataStructure_3::flip on an edge /// @param t_manifold The manifold containing the edge to flip /// @param to_be_moved The edge on which to try the move @@ -170,16 +198,15 @@ namespace ergodic_moves /// If successful, the triangulation is no longer Delaunay. /// @param t_manifold The simplicial manifold /// @returns The Expected (3,2) moved manifold or an Unexpected - [[nodiscard]] inline auto do_32_move(Manifold const& t_manifold) -> Expected + template + [[nodiscard]] inline auto do_32_move(Manifold const& t_manifold, + Generator& generator) -> Expected { -#ifndef NDEBUG - spdlog::debug("{} called.\n", __PRETTY_FUNCTION__); -#endif Delaunay triangulation{t_manifold.delaunay_snapshot()}; auto timelike_edges = foliated_triangulations::filter_edges<3>( foliated_triangulations::collect_edges<3>(triangulation), true); // Shuffle the container to create a random sequence of edges - std::ranges::shuffle(timelike_edges, utilities::make_random_generator()); + std::ranges::shuffle(timelike_edges, generator); // Try a (3,2) move on successive timelike edges in the sequence if (std::ranges::any_of(timelike_edges, [&](auto& edge) { return try_32_move(triangulation, edge); @@ -189,10 +216,27 @@ namespace ergodic_moves } // We've run out of edges to try std::string const msg = "No (3,2) move possible.\n"; - spdlog::warn(msg); return std::unexpected(msg); } // do_32_move() + /// @brief Propose one (3,2) site for Metropolis-Hastings. + /// @details The raw proposal domain is the set of timelike edges. Selecting + /// a nonflippable edge produces a self-transition. + template + [[nodiscard]] inline auto propose_32_move(Manifold const& t_manifold, + Generator& generator) -> Expected + { + auto triangulation = t_manifold.delaunay_snapshot(); + auto timelike_edges = foliated_triangulations::filter_edges<3>( + foliated_triangulations::collect_edges<3>(triangulation), true); + auto const candidate = detail::random_element(timelike_edges, generator); + if (candidate && try_32_move(triangulation, *candidate)) + { + return detail::make_manifold(std::move(triangulation), t_manifold); + } + return std::unexpected("Selected (3,2) proposal site is not movable.\n"); + } + /// @brief Find a (2,6) move location /// @details This function checks to see if a (2,6) move is possible. Starting /// with a (1,3) simplex, it checks neighbors for a (3,1) simplex. @@ -204,10 +248,6 @@ namespace ergodic_moves if (t_cell->info() != 13) { return std::nullopt; } // NOLINT for (auto i = 0; i < 4; ++i) { -#ifndef NDEBUG - spdlog::trace("Neighbor {} is of type {}\n", i, - t_cell->neighbor(i)->info()); -#endif if (foliated_triangulations::expected_cell_type<3>(t_cell->neighbor(i)) == Cell_type::THREE_ONE) { @@ -231,35 +271,42 @@ namespace ergodic_moves /// @image latex 26.eps width=7cm /// @param t_manifold The simplicial manifold /// @returns The Expected (2,6) moved manifold or an Unexpected - [[nodiscard]] inline auto do_26_move(Manifold const& t_manifold) -> Expected + template + [[nodiscard]] inline auto do_26_move_impl(Manifold const& t_manifold, + Generator& generator, + bool const only_first_site) + -> Expected { -#ifndef NDEBUG - spdlog::debug("{} called.\n", __PRETTY_FUNCTION__); -#endif static auto constexpr INCIDENT_CELLS_FOR_6_2_MOVE = 6; Delaunay triangulation{t_manifold.delaunay_snapshot()}; auto one_three = foliated_triangulations::filter_cells<3>( foliated_triangulations::collect_cells<3>(triangulation), Cell_type::ONE_THREE); - // Shuffle the container to pick a random sequence of (1,3) cells to try - std::ranges::shuffle(one_three, utilities::make_random_generator()); + if (only_first_site) + { + auto const candidate = detail::random_element(one_three, generator); + if (!candidate) + { + return std::unexpected("No (2,6) proposal site is available.\n"); + } + one_three = {*candidate}; + } + else + { + // Shuffle the container to pick a random sequence of (1,3) cells to try. + std::ranges::shuffle(one_three, generator); + } for (auto const& bottom : one_three) { if (auto neighboring_31_index = find_adjacent_31_cell(bottom); neighboring_31_index) { -#ifndef NDEBUG - spdlog::trace("neighboring_31_index is {}.\n", *neighboring_31_index); -#endif Cell_handle const top = bottom->neighbor(neighboring_31_index.value()); // Calculate the common face with respect to the bottom cell auto common_face_index = std::numeric_limits::max(); if (!bottom->has_neighbor(top, common_face_index)) { std::string const msg = "Bottom cell does not have a neighbor.\n"; -#ifndef NDEBUG - spdlog::trace(msg); -#endif return std::unexpected(msg); } @@ -282,9 +329,6 @@ namespace ergodic_moves if (v_1->info() != v_2->info() || v_2->info() != v_3->info()) { std::string const msg = "Vertices have different timeslices.\n"; -#ifndef NDEBUG - spdlog::trace(msg); -#endif return std::unexpected(msg); } @@ -302,9 +346,6 @@ namespace ergodic_moves { std::string const msg = "Center vertex is not bounded by 6 simplices.\n"; -#ifndef NDEBUG - spdlog::trace(msg); -#endif return std::unexpected(msg); } @@ -317,56 +358,49 @@ namespace ergodic_moves !check_cells) { std::string const msg = "A cell is invalid.\n"; -#ifndef NDEBUG - spdlog::trace(msg); -#endif return std::unexpected(msg); } // Now assign a geometric point to the center vertex - auto center_point = + auto const center_point = CGAL::centroid(v_1->point(), v_2->point(), v_3->point()); -#ifndef NDEBUG - spdlog::trace("Center point is: ({}).\n", - utilities::point_to_str(center_point)); -#endif v_center->set_point(center_point); // Assign a timevalue to the new vertex auto timevalue = v_1->info(); v_center->info() = timevalue; -#ifndef NDEBUG - spdlog::trace("Spacelike face timevalue is {}.\n", timevalue); - spdlog::trace("Inserted vertex ({}) with timevalue {}.\n", - utilities::point_to_str(v_center->point()), - v_center->info()); -#endif - // Final checks // is_valid() checks for combinatorial and geometric validity if (!triangulation.tds().is_valid(v_center, true, 1)) { std::string const msg = "v_center is invalid.\n"; -#ifndef NDEBUG - spdlog::trace(msg); -#endif return std::unexpected(msg); } return detail::make_manifold(std::move(triangulation), t_manifold); } - // Try next cell -#ifndef NDEBUG - spdlog::debug("Cell not insertable.\n"); -#endif } // We've run out of (1,3) simplices to try std::string const msg = "No (2,6) move possible.\n"; - spdlog::warn(msg); return std::unexpected(msg); } // do_26_move() + /// @brief Perform a (2,6) move using a caller-owned random stream. + template + [[nodiscard]] inline auto do_26_move(Manifold const& t_manifold, + Generator& generator) -> Expected + { return do_26_move_impl(t_manifold, generator, false); } + + /// @brief Propose a uniformly selected (2,6) site. + /// @details Exactly one uniformly selected (1,3) cell is examined. An + /// inapplicable raw site is returned as a failed proposal so Metropolis- + /// Hastings can account for it as a self-transition. + template + [[nodiscard]] inline auto propose_26_move(Manifold const& t_manifold, + Generator& generator) -> Expected + { return do_26_move_impl(t_manifold, generator, true); } + /// @brief Find a (6,2) move location /// @details This function checks to see if a (6,2) move is possible. Starting /// with a vertex, it checks all incident cells. There must be 6 @@ -379,29 +413,14 @@ namespace ergodic_moves Vertex_handle const& candidate) -> bool { - if (triangulation.dimension() != 3) - { -#ifndef NDEBUG - spdlog::trace("Manifold is not 3-dimensional.\n"); -#endif - return false; - } + if (triangulation.dimension() != 3) { return false; } - if (!triangulation.tds().is_vertex(candidate)) - { -#ifndef NDEBUG - spdlog::trace("Candidate is not a vertex.\n"); -#endif - return false; - } + if (!triangulation.tds().is_vertex(candidate)) { return false; } // We must have 5 incident edges to have 6 incident cells if (auto incident_edges = triangulation.degree(candidate); incident_edges != 5) // NOLINT { -#ifndef NDEBUG - spdlog::trace("Vertex has {} incident edges.\n", incident_edges); -#endif return false; } @@ -413,22 +432,13 @@ namespace ergodic_moves // We must have 6 cells incident to the vertex to make a (6,2) move if (incident_cells.size() != 6) // NOLINT { -#ifndef NDEBUG - spdlog::trace("Vertex has {} incident cells.\n", incident_cells.size()); -#endif return false; } // Check that none of the incident cells are infinite for (auto const& cell : incident_cells) { - if (triangulation.is_infinite(cell)) - { -#ifndef NDEBUG - spdlog::trace("Cell is infinite.\n"); -#endif - return false; - } + if (triangulation.is_infinite(cell)) { return false; } } auto const cell_type_count = [&](Cell_type const type) { @@ -446,13 +456,6 @@ namespace ergodic_moves spdlog::warn("Some incident cells on this vertex need to be fixed.\n"); } -#ifndef NDEBUG - spdlog::trace( - "Vertex has {} incident cells with {} incident (3,1) simplices and {} " - "incident (2,2) simplices and {} incident (1,3) simplices.\n", - incident_cells.size(), incident_31, incident_22, incident_13); - foliated_triangulations::debug_print_cells<3>(std::span{incident_cells}); -#endif return incident_31 == 3 && incident_22 == 0 && incident_13 == 3; } // find_62_moves() @@ -464,10 +467,13 @@ namespace ergodic_moves /// Keeping both operations in a private copy makes rejection failure-atomic. /// @param source_triangulation The triangulation containing the candidate /// @param source_candidate The degree-five vertex to remove + /// @param generator The caller-owned random engine used to order flip paths /// @returns The moved triangulation, or nullopt when the topology is not /// flippable or the result violates a triangulation or causal-cell invariant + template [[nodiscard]] inline auto try_62_move(Delaunay const& source_triangulation, - Vertex_handle const source_candidate) + Vertex_handle const source_candidate, + Generator& generator) -> std::optional { if (!source_triangulation.tds().is_vertex(source_candidate) || @@ -490,7 +496,7 @@ namespace ergodic_moves Edge_container incident_edges; triangulation.finite_incident_edges(candidate, std::back_inserter(incident_edges)); - std::ranges::shuffle(incident_edges, utilities::make_random_generator()); + std::ranges::shuffle(incident_edges, generator); auto const is_timelike = [](Edge_handle const& edge) { auto const first_time = edge.first->vertex(edge.second)->info(); @@ -544,30 +550,46 @@ namespace ergodic_moves /// /// @param t_manifold The simplicial manifold /// @returns The Expected (6,2) moved manifold or Unexpected - [[nodiscard]] inline auto do_62_move(Manifold const& t_manifold) -> Expected + template + [[nodiscard]] inline auto do_62_move(Manifold const& t_manifold, + Generator& generator) -> Expected { -#ifndef NDEBUG - spdlog::debug("{} called.\n", __PRETTY_FUNCTION__); -#endif auto triangulation = t_manifold.delaunay_snapshot(); auto vertices = foliated_triangulations::collect_vertices<3>(triangulation); // Shuffle the container to create a random sequence of vertices - std::ranges::shuffle(vertices, utilities::make_random_generator()); + std::ranges::shuffle(vertices, generator); // Try a (6,2) move on successive vertices in the sequence for (auto const& vertex : vertices) { if (!is_62_movable(triangulation, vertex)) { continue; } - if (auto moved = try_62_move(triangulation, vertex)) + if (auto moved = try_62_move(triangulation, vertex, generator)) { return detail::make_manifold(std::move(*moved), t_manifold); } } // We've run out of vertices to try std::string const msg = "No (6,2) move possible.\n"; - spdlog::warn(msg); return std::unexpected(msg); } // do_62_move() + /// @brief Propose one vertex as a (6,2) site for Metropolis-Hastings. + template + [[nodiscard]] inline auto propose_62_move(Manifold const& t_manifold, + Generator& generator) -> Expected + { + auto triangulation = t_manifold.delaunay_snapshot(); + auto vertices = foliated_triangulations::collect_vertices<3>(triangulation); + auto const candidate = detail::random_element(vertices, generator); + if (candidate && is_62_movable(triangulation, *candidate)) + { + if (auto moved = try_62_move(triangulation, *candidate, generator)) + { + return detail::make_manifold(std::move(*moved), t_manifold); + } + } + return std::unexpected("Selected (6,2) proposal site is not movable.\n"); + } + /// @brief Find all cells incident to the edge /// @param triangulation The Delaunay triangulation /// @param edge The edge @@ -596,9 +618,6 @@ namespace ergodic_moves incident_cells.emplace_back(circulator); } while (++circulator != edge.first); -#ifndef NDEBUG - spdlog::trace("Found {} incident cells on edge.\n", incident_cells.size()); -#endif return incident_cells; } // incident_cells_from_edge() @@ -651,6 +670,7 @@ namespace ergodic_moves /// @param source_top Top vertex of the cells being flipped /// @param source_bottom Bottom vertex of the cells being flipped /// @returns A flipped triangulation or nullopt + /// @see [Pachner moves](../REFERENCES.md#pachner-moves) [[nodiscard]] inline auto bistellar_flip(Delaunay const& source_triangulation, Edge_handle const source_edge, Vertex_handle const source_top, @@ -790,10 +810,6 @@ namespace ergodic_moves { if (auto incident_cells = incident_cells_from_edge(triangulation, edge)) { -#ifndef NDEBUG - fmt::print("Edge has {} incident finite cells\n", - incident_cells->size()); -#endif if (incident_cells->size() == 4) { return edge; } } } @@ -834,16 +850,15 @@ namespace ergodic_moves /// /// @param t_manifold The simplicial manifold /// @return The Expected (4,4) moved manifold or Unexpected - [[nodiscard]] inline auto do_44_move(Manifold const& t_manifold) -> Expected + template + [[nodiscard]] inline auto do_44_move(Manifold const& t_manifold, + Generator& generator) -> Expected { -#ifndef NDEBUG - spdlog::debug("{} called.\n", __PRETTY_FUNCTION__); -#endif auto triangulation = t_manifold.delaunay_snapshot(); auto spacelike_edges = foliated_triangulations::filter_edges<3>( foliated_triangulations::collect_edges<3>(triangulation), false); // Shuffle the container to pick a random sequence of edges to try - std::ranges::shuffle(spacelike_edges, utilities::make_random_generator()); + std::ranges::shuffle(spacelike_edges, generator); for (auto const& edge : spacelike_edges) { // Obtain all incident cells @@ -851,13 +866,6 @@ namespace ergodic_moves find_bistellar_flip_location(triangulation, edge); incident_cells) { -#ifndef NDEBUG - for (auto const& cell : *incident_cells) - { - spdlog::trace("Incident cell is of type {}.\n", cell->info()); - } -#endif - // Get edge vertices auto const& v1 = edge.first->vertex(edge.second); auto const& v2 = edge.first->vertex(edge.third); @@ -896,20 +904,65 @@ namespace ergodic_moves t_manifold); } - // If we get here, the flip failed but we found potential cells - // Log the reason and continue trying other edges -#ifndef NDEBUG - spdlog::debug("Bistellar flip failed."); -#endif + // The flip failed; continue trying other edges. } // Try next edge } // We've run out of edges to try std::string const msg = "No (4,4) move possible.\n"; - spdlog::warn(msg); return std::unexpected(msg); } // do_44_move() + /// @brief Propose one spacelike edge as a (4,4) site. + /// @details Selecting an edge that is not the pivot of a causal four-cell + /// complex is an explicit self-transition. + template + [[nodiscard]] inline auto propose_44_move(Manifold const& t_manifold, + Generator& generator) -> Expected + { + auto triangulation = t_manifold.delaunay_snapshot(); + auto spacelike_edges = foliated_triangulations::filter_edges<3>( + foliated_triangulations::collect_edges<3>(triangulation), false); + auto const candidate = detail::random_element(spacelike_edges, generator); + if (!candidate) + { + return std::unexpected( + "No spacelike edge is available for a (4,4) proposal.\n"); + } + + auto const incident_cells = + find_bistellar_flip_location(triangulation, *candidate); + if (!incident_cells) + { + return std::unexpected("Selected (4,4) proposal site is not movable.\n"); + } + + auto const& v1 = candidate->first->vertex(candidate->second); + auto const& v2 = candidate->first->vertex(candidate->third); + Vertex_handle top = nullptr; + Vertex_handle bottom = nullptr; + for (auto const& cell : *incident_cells) + { + for (int index = 0; index < 4; ++index) + { + auto const vertex = cell->vertex(index); + if (vertex == v1 || vertex == v2) { continue; } + if (top == nullptr || vertex->info() > top->info()) { top = vertex; } + if (bottom == nullptr || vertex->info() < bottom->info()) + { + bottom = vertex; + } + } + } + + if (auto flipped = bistellar_flip(triangulation, *candidate, top, bottom)) + { + return detail::make_manifold(std::move(*flipped), t_manifold); + } + return std::unexpected( + "Selected (4,4) proposal site could not be flipped.\n"); + } + /// @brief Check move correctness /// @param t_before The manifold before the move /// @param t_after The manifold after the move diff --git a/include/Foliated_triangulation.hpp b/include/Foliated_triangulation.hpp index 5af1967237..5103e1fa1a 100644 --- a/include/Foliated_triangulation.hpp +++ b/include/Foliated_triangulation.hpp @@ -21,6 +21,8 @@ #ifndef CDT_PLUSPLUS_FOLIATEDTRIANGULATION_HPP #define CDT_PLUSPLUS_FOLIATEDTRIANGULATION_HPP +#include + #include #include #include @@ -29,6 +31,7 @@ #include #include +#include "Random.hpp" #include "Triangulation_traits.hpp" #include "Utilities.hpp" @@ -903,11 +906,12 @@ namespace foliated_triangulations /// @param initial_radius The radius of the first time slice /// @param foliation_spacing The distance between successive time slices /// @return A container of (vertex, timevalue) pairs - template - [[nodiscard]] auto make_foliated_ball( - Int_precision const t_simplices, Int_precision const t_timeslices, - double const initial_radius = INITIAL_RADIUS, - double const foliation_spacing = FOLIATION_SPACING) + template + [[nodiscard]] auto make_foliated_ball(Int_precision const t_simplices, + Int_precision const t_timeslices, + double const initial_radius, + double const foliation_spacing, + Generator& generator) { if (t_simplices < 2 || t_timeslices < 2) { @@ -943,6 +947,8 @@ namespace foliated_triangulations Causal_vertices_t causal_vertices; causal_vertices.reserve(static_cast(t_simplices)); + std::uniform_int_distribution seed_distribution; + CGAL::Random cgal_random{seed_distribution(generator)}; for (gsl::index i = 0; i < t_timeslices; ++i) { @@ -961,7 +967,7 @@ namespace foliated_triangulations throw std::out_of_range( "Foliation parameters generate too many points per timeslice."); } - Spherical_points_generator_t gen{radius}; + Spherical_points_generator_t gen{radius, cgal_random}; // Generate random points at the radius for (gsl::index j = 0; j < static_cast(generated_points); ++j) @@ -983,11 +989,13 @@ namespace foliated_triangulations /// @param initial_radius Radius of first timeslice /// @param foliation_spacing Radial separation between timeslices /// @return A Delaunay triangulation with a timevalue for each vertex - template - [[nodiscard]] auto make_triangulation( - Int_precision const t_simplices, Int_precision t_timeslices, - double const initial_radius = INITIAL_RADIUS, - double const foliation_spacing = FOLIATION_SPACING) + /// @see [CGAL triangulations](../REFERENCES.md#cgal-triangulations) + template + [[nodiscard]] auto make_triangulation(Int_precision const t_simplices, + Int_precision t_timeslices, + double const initial_radius, + double const foliation_spacing, + Generator& generator) -> Delaunay_t { #ifndef NDEBUG @@ -1010,8 +1018,9 @@ namespace foliated_triangulations #endif // Make initial triangulation - auto causal_vertices = make_foliated_ball( - t_simplices, t_timeslices, initial_radius, foliation_spacing); + auto causal_vertices = + make_foliated_ball(t_simplices, t_timeslices, initial_radius, + foliation_spacing, generator); triangulation.insert(causal_vertices.begin(), causal_vertices.end()); // Fix vertices @@ -1236,21 +1245,28 @@ namespace foliated_triangulations , m_min_timevalue{find_min_timevalue<3>(std::span{m_vertices})} {} - /// @brief Constructor with parameters - /// @param t_simplices Number of desired simplices - /// @param t_timeslices Number of desired timeslices - /// @param t_initial_radius Radius of first timeslice - /// @param t_foliation_spacing Radial separation between timeslices + /// @brief Constructor with a caller-owned initialization stream. FoliatedTriangulation(Int_precision const t_simplices, Int_precision const t_timeslices, + cdt::Random& generator, double const t_initial_radius = INITIAL_RADIUS, double const t_foliation_spacing = FOLIATION_SPACING) : FoliatedTriangulation{ make_triangulation<3>(t_simplices, t_timeslices, t_initial_radius, - t_foliation_spacing), + t_foliation_spacing, generator), t_initial_radius, t_foliation_spacing} {} + /// @brief Construct from an explicit temporary initialization stream. + FoliatedTriangulation(Int_precision const t_simplices, + Int_precision const t_timeslices, + cdt::Random&& generator, + double const t_initial_radius = INITIAL_RADIUS, + double const t_foliation_spacing = FOLIATION_SPACING) + : FoliatedTriangulation{t_simplices, t_timeslices, generator, + t_initial_radius, t_foliation_spacing} + {} + /// @brief Constructor from Causal_vertices /// @param causal_vertices Causal_vertices to place into the /// FoliatedTriangulation diff --git a/include/Manifold.hpp b/include/Manifold.hpp index 35734b0cfb..0882619f7d 100644 --- a/include/Manifold.hpp +++ b/include/Manifold.hpp @@ -16,6 +16,7 @@ #include #include "Geometry.hpp" +#include "Random.hpp" namespace manifolds { @@ -107,21 +108,26 @@ namespace manifolds , m_geometry{m_triangulation} {} - /// @brief Construct manifold using arguments - /// @param t_desired_simplices Number of desired simplices - /// @param t_desired_timeslices Number of desired timeslices - /// @param t_initial_radius Radius of first timeslice - /// @param t_foliation_spacing Radial separation between timeslices + /// @brief Construct a manifold with a caller-owned initialization stream. Manifold(Int_precision const t_desired_simplices, - Int_precision const t_desired_timeslices, - double const t_initial_radius = INITIAL_RADIUS, - double const t_foliation_spacing = FOLIATION_SPACING) + Int_precision const t_desired_timeslices, cdt::Random& generator, + double const t_initial_radius = INITIAL_RADIUS, + double const t_foliation_spacing = FOLIATION_SPACING) : Manifold{ Triangulation{t_desired_simplices, t_desired_timeslices, - t_initial_radius, t_foliation_spacing} + generator, t_initial_radius, t_foliation_spacing} } {} + /// @brief Construct from an explicit temporary initialization stream. + Manifold(Int_precision const t_desired_simplices, + Int_precision const t_desired_timeslices, cdt::Random&& generator, + double const t_initial_radius = INITIAL_RADIUS, + double const t_foliation_spacing = FOLIATION_SPACING) + : Manifold{t_desired_simplices, t_desired_timeslices, generator, + t_initial_radius, t_foliation_spacing} + {} + /// @brief Construct manifold from Causal_vertices /// Pass-by-value-then-move. /// @param causal_vertices Causal_vertices to place into the Manifold diff --git a/include/Metropolis.hpp b/include/Metropolis.hpp index 7536fb7839..cfa9b5ea3a 100644 --- a/include/Metropolis.hpp +++ b/include/Metropolis.hpp @@ -8,31 +8,38 @@ /// @brief Perform Metropolis-Hastings algorithm on Delaunay Triangulations /// @author Adam Getchell /// @details Performs the Metropolis-Hastings algorithm on the foliated Delaunay -/// triangulations. For details see: -/// M. Creutz, and B. Freedman. “A Statistical Approach to Quantum Mechanics.” -/// Annals of Physics 132 (1981): 427–62. -/// @see http://thy.phy.bnl.gov/~creutz/mypubs/pub044.pdf +/// triangulations. +/// @see [Metropolis-Hastings +/// algorithm](../REFERENCES.md#metropolis-hastings-algorithm) +/// @see [Three-dimensional CDT](../REFERENCES.md#three-dimensional-cdt-2001) /// @todo Implement concurrency #ifndef INCLUDE_METROPOLIS_HPP_ #define INCLUDE_METROPOLIS_HPP_ +#include +#include +#include #include +#include // CDT headers -#include "Move_command.hpp" +#include "Ergodic_moves_3.hpp" #include "Move_strategy.hpp" +#include "Random.hpp" #include "S3Action.hpp" -using Gmpzf = CGAL::Gmpzf; - /// @brief Metropolis-Hastings algorithm strategy /// @details The Metropolis-Hastings algorithm is a Markov Chain Monte Carlo -/// method. The probability of making an ergodic (Pachner) move is: +/// method. For target weight \f$\pi(T)\propto e^{-S(T)}\f$, a proposal from +/// triangulation \f$T\f$ to \f$T'\f$ is accepted with probability: +/// +/// \f[\min\left(1, e^{S(T)-S(T')} +/// \frac{q(T\mid T')}{q(T'\mid T)}\right).\f] /// -/// \f[P_{ergodic move}=a_{1}a_{2}\f] -/// \f[a_1=\frac{move[i]}{\sum\limits_{i}move[i]}\f] -/// \f[a_2=e^{\Delta S}\f] +/// The proposal-ratio construction follows the Hastings transition rule; see +/// [Metropolis-Hastings +/// algorithm](../REFERENCES.md#metropolis-hastings-algorithm). /// /// @tparam ManifoldType The type of Manifold on which to apply the algorithm template @@ -67,25 +74,30 @@ class MoveStrategy /// @brief The current geometry of the manifold Geometry m_geometry; - /// @brief The number of moves the algorithm tried + /// @brief Run-owned random engine used for move, site, and acceptance draws + cdt::Random m_generator; + + /// @brief The number of move types and raw sites proposed /// @details This equals accepted moves + rejected moves. Counter m_proposed_moves; - /// @brief The number of moves accepted by the algorithm + /// @brief The number of proposals committed as state transitions Counter m_accepted_moves; - /// @brief The number of moves rejected by the algorithm + /// @brief The number of explicit self-transitions + /// @details Includes inapplicable sites, failed candidate construction, and + /// candidates rejected by the Metropolis-Hastings draw. Counter m_rejected_moves; - /// @brief The number of moves that were attempted by a MoveCommand. - /// @details This should equal accepted moves. + /// @brief The number of proposal sites whose construction was attempted + /// @details This equals proposed moves. Counter m_attempted_moves; - /// @brief The number of moves that succeeded in the MoveCommand + /// @brief The number of attempts that produced a valid candidate manifold + /// @details A successful candidate may still be rejected by MH. Counter m_succeeded_moves; - /// @brief The number of moves that a MoveCommand failed to make due to an - /// error + /// @brief The number of inapplicable or invalid candidate constructions Counter m_failed_moves; public: @@ -106,7 +118,20 @@ class MoveStrategy Int_precision const passes, Int_precision const checkpoint, bool const write_files = true) - : m_passes(passes), m_checkpoint{checkpoint}, m_write_files{write_files} + : MoveStrategy{Alpha, K, Lambda, passes, + checkpoint, write_files, cdt::Random{}} + {} + + /// @brief Construct a run from an already selected PCG stream. + [[maybe_unused]] MoveStrategy(long double const Alpha, long double const K, + long double const Lambda, + Int_precision const passes, + Int_precision const checkpoint, + bool const write_files, cdt::Random random) + : m_passes(passes) + , m_checkpoint{checkpoint} + , m_write_files{write_files} + , m_generator{std::move(random)} { auto const parameters = s3_action::make_physical_parameters(Alpha, K, Lambda); @@ -127,6 +152,15 @@ class MoveStrategy #endif } + /// @brief Construct a replayable run with an explicit RNG seed. + MoveStrategy(long double const Alpha, long double const K, + long double const Lambda, Int_precision const passes, + Int_precision const checkpoint, bool const write_files, + std::uint64_t const seed) + : MoveStrategy{Alpha, K, Lambda, passes, + checkpoint, write_files, cdt::Random{seed}} + {} + /// @returns The length of the timelike edge [[nodiscard]] auto Alpha() const noexcept { return m_Alpha; } @@ -145,6 +179,12 @@ class MoveStrategy /// @returns Whether the strategy writes checkpoint triangulation files [[nodiscard]] auto writes_files() const noexcept { return m_write_files; } + /// @returns The effective root seed used for this run. + [[nodiscard]] auto seed() const noexcept { return m_generator.seed(); } + + /// @returns The PCG stream selector used for transitions. + [[nodiscard]] auto stream() const noexcept { return m_generator.stream(); } + /// @returns The container of trial moves auto get_proposed() const { return m_proposed_moves; } @@ -163,200 +203,175 @@ class MoveStrategy /// @returns The container of failed moves auto get_failed() const { return m_failed_moves; } - /// @brief Calculate A1 - /// @details Calculate the probability of making a move divided by the - /// probability of its reverse, that is: - /// \f[a_1=\frac{move[i]}{\sum\limits_{i}move[i]}\f] - /// - /// @param move The type of move - /// @returns \f$a_1=\frac{move[i]}{\sum\limits_{i}move[i]}\f$ - [[nodiscard]] auto CalculateA1(move_tracker::move_type move) const - { - auto const all_moves = m_proposed_moves.total(); - auto const this_move = m_proposed_moves[move]; - auto const proposed_move = mpfr_values::from_integer(this_move); - auto const proposed_total = mpfr_values::from_integer(all_moves); - auto const probability = mpfr_values::divide(proposed_move, proposed_total); - auto const result = mpfr_values::to_long_double(probability); + /// @returns The geometry used by the most recent acceptance decision + [[nodiscard]] auto get_geometry() const noexcept + -> Geometry const& + { return m_geometry; } -#ifndef NDEBUG - spdlog::debug("{} called.\n", __PRETTY_FUNCTION__); - spdlog::trace("Total proposed moves = {}\n", all_moves); - spdlog::trace("A1 is {}\n", result); -#endif - - return result; - } // CalculateA1() - - /// @brief Calculate A2 - /// @details Calculate \f$a_2=e^{\Delta S}\f$ - /// @param move The type of move - /// @returns \f$a_2=e^{-\Delta S}\f$ - [[nodiscard]] auto CalculateA2(move_tracker::move_type const move) const + /// @returns The inverse Pachner move + [[nodiscard]] static auto constexpr reverse_move( + move_tracker::move_type const move) noexcept -> move_tracker::move_type { - auto currentS3Action = - S3_bulk_action(m_geometry.N1_TL, m_geometry.N3_31_13, m_geometry.N3_22, - m_Alpha, m_K, m_Lambda); - auto newS3Action = static_cast(0); + using enum move_tracker::move_type; switch (move) { - case move_tracker::move_type::TWO_THREE: - // A (2,3) move adds a timelike edge - // and a (2,2) simplex - newS3Action = - S3_bulk_action(m_geometry.N1_TL + 1, m_geometry.N3_31_13, - m_geometry.N3_22 + 1, m_Alpha, m_K, m_Lambda); - break; - case move_tracker::move_type::THREE_TWO: - // A (3,2) move removes a timelike edge - // and a (2,2) simplex - newS3Action = - S3_bulk_action(m_geometry.N1_TL - 1, m_geometry.N3_31_13, - m_geometry.N3_22 - 1, m_Alpha, m_K, m_Lambda); - break; - case move_tracker::move_type::TWO_SIX: - // A (2,6) move adds 2 timelike edges and - // 2 (1,3) and 2 (3,1) simplices - newS3Action = - S3_bulk_action(m_geometry.N1_TL + 2, m_geometry.N3_31_13 + 4, - m_geometry.N3_22, m_Alpha, m_K, m_Lambda); - break; - case move_tracker::move_type::SIX_TWO: - // A (6,2) move removes 2 timelike edges and - // 2 (1,3) and 2 (3,1) simplices - newS3Action = - S3_bulk_action(m_geometry.N1_TL - 2, m_geometry.N3_31_13 - 4, - m_geometry.N3_22, m_Alpha, m_K, m_Lambda); - break; - case move_tracker::move_type::FOUR_FOUR: - // A (4,4) move changes nothing with respect to the action, - // and e^0==1 -#ifndef NDEBUG - spdlog::trace("A2 is 1\n"); -#endif - return static_cast(1); - default: break; + case TWO_THREE: return THREE_TWO; + case THREE_TWO: return TWO_THREE; + case TWO_SIX: return SIX_TWO; + case SIX_TWO: return TWO_SIX; + case FOUR_FOUR: return FOUR_FOUR; } + return FOUR_FOUR; + } - auto exponent = currentS3Action - newS3Action; - auto exponent_double = utilities::Gmpzf_to_double(exponent); - - // if exponent > 0 then e^exponent >=1 so according to Metropolis - // algorithm return A2=1 - if (exponent >= 0) { return static_cast(1); } - - auto const exponent_value = mpfr_values::from_long_double(exponent_double); - auto const probability = mpfr_values::exponential(exponent_value); - auto const result = mpfr_values::to_long_double(probability); - -#ifndef NDEBUG - spdlog::trace("A2 is {}\n", result); -#endif - - return result; - } // CalculateA2() - - /// @brief Try a move of the selected type - /// @details This function implements the core of the Metropolis-Hastings - /// algorithm by generating a random number and comparing with the results - /// of CalculateA1 and CalculateA2. - /// @param move The type of move - /// @returns True if the move is accepted - auto try_move(move_tracker::move_type const move) -> bool + /// @returns The number of raw sites from which a move type is proposed + [[nodiscard]] static auto constexpr proposal_site_count( + Geometry const& geometry, + move_tracker::move_type const move) noexcept -> Int_precision { - // Record the proposed move - ++m_proposed_moves[as_integer(move)]; - - // Calculate probability - auto a_1 = CalculateA1(move); + using enum move_tracker::move_type; + switch (move) + { + case TWO_THREE: return geometry.N3_22; + case THREE_TWO: return geometry.N1_TL; + case TWO_SIX: return geometry.N3_13; + case SIX_TWO: return geometry.N0; + case FOUR_FOUR: return geometry.N1_SL; + } + return 0; + } - // Make move if random number < probability - auto a_2 = CalculateA2(move); + /// @returns The probability of selecting a particular raw proposal site + /// @details Move types are uniform. A raw site is then uniform within its + /// type-specific domain. Inapplicable sites remain explicit self-transitions. + [[nodiscard]] static auto proposal_probability( + Geometry const& geometry, + move_tracker::move_type const move) -> mpfr_values::Value + { + auto const sites = proposal_site_count(geometry, move); + if (sites <= 0) { return mpfr_values::zero(); } + auto const move_count = + mpfr_values::from_integer(move_tracker::NUMBER_OF_3D_MOVES); + auto const site_count = mpfr_values::from_integer(sites); + auto const denominator = mpfr_values::multiply(move_count, site_count); + return mpfr_values::divide(mpfr_values::from_integer(1), denominator); + } - if (auto const trial_value = utilities::generate_probability(); - trial_value <= a_1 * a_2) + /// @brief Calculate the Hastings reverse-to-forward proposal ratio + /// @see [Metropolis-Hastings + /// algorithm](../REFERENCES.md#metropolis-hastings-algorithm) + [[nodiscard]] static auto CalculateA1( + Geometry const& current, + Geometry const& proposed, + move_tracker::move_type const move) -> mpfr_values::Value + { + auto const forward = proposal_probability(current, move); + auto const reverse = proposal_probability(proposed, reverse_move(move)); + if (mpfr_zero_p(forward.fr()) != 0 || mpfr_zero_p(reverse.fr()) != 0) { -#ifndef NDEBUG - spdlog::debug("{} called.\n", __PRETTY_FUNCTION__); - spdlog::trace("Trying move.\n"); - spdlog::trace("Move type = {}\n", as_integer(move)); - spdlog::trace("Trial_value = {}\n", trial_value); - spdlog::trace("A1 = {}\n", a_1); - spdlog::trace("A2 = {}\n", a_2); - spdlog::trace("A1*A2 = {}\n", a_1 * a_2); - spdlog::trace("{}\n", trial_value <= a_1 * a_2 ? "Move accepted." - : "Move rejected."); -#endif - // Accept the move - ++m_accepted_moves[as_integer(move)]; - return true; + throw std::logic_error( + "A successful reversible proposal must have nonzero forward and reverse probabilities."); } + return mpfr_values::divide(reverse, forward); + } - // Reject the move - ++m_rejected_moves[as_integer(move)]; - return false; + /// @brief Calculate the action factor \f$e^{S(T)-S(T')}\f$ + /// @see [Three-dimensional CDT + /// action](../REFERENCES.md#three-dimensional-cdt-2001) + [[nodiscard]] auto CalculateA2( + Geometry const& current, + Geometry const& proposed) const + -> mpfr_values::Value + { + auto const current_action = S3_bulk_action( + current.N1_TL, current.N3_31_13, current.N3_22, m_Alpha, m_K, m_Lambda); + auto const proposed_action = + S3_bulk_action(proposed.N1_TL, proposed.N3_31_13, proposed.N3_22, + m_Alpha, m_K, m_Lambda); + return mpfr_values::exponential( + mpfr_values::subtract(current_action, proposed_action)); + } - } // try_move() - - /// @brief Initialize the Metropolis algorithm - /// @details This function initializes the Metropolis algorithm by - /// making a move of each type, so that when A1 is calculated - /// we don't have divide by zero - /// @param t_manifold Manifold on which to operate - /// @returns A manifold with a move of each type completed - [[nodiscard]] auto initialize(ManifoldType t_manifold) - -> std::optional> - try + /// @returns \f$\min(1, q(T|T')/q(T'|T)e^{S(T)-S(T')})\f$ + [[nodiscard]] auto acceptance_probability( + Geometry const& current, + Geometry const& proposed, + move_tracker::move_type const move) const -> mpfr_values::Value { - MoveCommand command(t_manifold); - fmt::print("Making initial moves ...\n"); + auto const ratio = mpfr_values::multiply( + CalculateA1(current, proposed, move), CalculateA2(current, proposed)); + auto const one = mpfr_values::from_integer(1); + return mpfr_cmp(ratio.fr(), one.fr()) < 0 ? ratio : one; + } - // Make a move of each type - for (auto move = 0; move < move_tracker::NUMBER_OF_3D_MOVES; ++move) + private: + [[nodiscard]] auto propose_candidate(ManifoldType const& current, + move_tracker::move_type const move) + -> std::expected + { + using enum move_tracker::move_type; + switch (move) { -#ifndef NDEBUG - spdlog::trace("Making move {} ...\n", move); -#endif - command.enqueue(move_tracker::as_move(move)); - ++m_proposed_moves[move]; - ++m_accepted_moves[move]; + case TWO_THREE: + return ergodic_moves::propose_23_move(current, m_generator); + case THREE_TWO: + return ergodic_moves::propose_32_move(current, m_generator); + case TWO_SIX: return ergodic_moves::propose_26_move(current, m_generator); + case SIX_TWO: return ergodic_moves::propose_62_move(current, m_generator); + case FOUR_FOUR: + return ergodic_moves::propose_44_move(current, m_generator); } + return std::unexpected("Unknown 3D Pachner move.\n"); + } - // Execute the initial moves - command.execute(); - command.print_successful(); - command.print_errors(); + public: + /// @brief Attempt and immediately resolve one Markov transition + /// @param current Canonical state, updated only after a successful MH accept + /// @param move Uniformly selected move type + /// @param trial_value Uniform draw in [0,1], injectable for focused tests + /// @returns True only when a valid candidate is accepted and committed + auto attempt_transition(ManifoldType& current, + move_tracker::move_type const move, + long double const trial_value) -> bool + { + if (!std::isfinite(trial_value) || trial_value < 0.0L || trial_value > 1.0L) + { + throw std::invalid_argument("MH trial value must lie in [0, 1]."); + } - // Update attempted, succeeded, and failed moves from MoveCommand - m_attempted_moves += command.get_attempted(); - m_succeeded_moves += command.get_succeeded(); - m_failed_moves += command.get_failed(); + m_geometry = current.get_geometry(); + ++m_proposed_moves[move]; + ++m_attempted_moves[move]; - // Reset the move counters - command.reset_counters(); + auto candidate = propose_candidate(current, move); + if (!candidate || !candidate->is_correct() || + !ergodic_moves::check_move(current, *candidate, move)) + { + ++m_failed_moves[move]; + ++m_rejected_moves[move]; + return false; + } - // print initial results - auto initial_results = command.get_results(); - initial_results.print(); - initial_results.print_details(); + ++m_succeeded_moves[move]; + auto const probability = + acceptance_probability(m_geometry, candidate->get_geometry(), move); + if (mpfr_cmp_ld(probability.fr(), trial_value) >= 0) + { + swap(*candidate, current); + m_geometry = current.get_geometry(); + ++m_accepted_moves[move]; + return true; + } - return command; - } - catch (std::system_error const& SystemError) - { - spdlog::debug("Metropolis initialization failed with {} ... exiting.\n", - SystemError.what()); - spdlog::trace("{}\n", SystemError.code().message()); - return std::nullopt; + ++m_rejected_moves[move]; + return false; } - /// @brief Call operator - /// @details This makes the Metropolis class into a function object. Setup - /// of the runtime job parameters is handled by the constructor. This () - /// operator conducts all of the algorithmic work for Metropolis-Hastings on - /// the manifold. - /// @param t_manifold Manifold on which to operate - /// @returns The manifold upon which the passes have been completed + /// @brief Initialize the cached action geometry from the canonical manifold + void initialize(ManifoldType const& manifold) + { m_geometry = manifold.get_geometry(); } + + /// @brief Run sequential Metropolis-Hastings passes on a manifold auto operator()(ManifoldType const& t_manifold) -> ManifoldType { #ifndef NDEBUG @@ -366,6 +381,8 @@ class MoveStrategy fmt::print( "Starting Metropolis-Hastings algorithm in {}+1 dimensions ...\n", ManifoldType::dimension - 1); + fmt::print("Effective random seed: {} (stream {}).\n", m_generator.seed(), + m_generator.stream()); m_proposed_moves.reset(); m_accepted_moves.reset(); @@ -374,55 +391,37 @@ class MoveStrategy m_succeeded_moves.reset(); m_failed_moves.reset(); - auto initialized = initialize(t_manifold); - auto command = initialized ? std::move(*initialized) - : MoveCommand{t_manifold}; + auto current = t_manifold; + initialize(current); + std::uniform_real_distribution acceptance_draw{0.0L, 1.0L}; fmt::print("Making random moves ...\n"); - - // Loop through m_passes for (auto pass_number = 1; pass_number <= m_passes; ++pass_number) { fmt::print("=== Pass {} ===\n", pass_number); - auto total_simplices_this_pass = command.get_const_results().N3(); - // Attempt a random move per simplex - for (auto move_attempt = 0; move_attempt < total_simplices_this_pass; + auto const attempts_this_pass = current.N3(); + for (auto move_attempt = 0; move_attempt < attempts_this_pass; ++move_attempt) { - // Pick a move to attempt - - if (auto move = move_tracker::generate_random_move_3(); try_move(move)) - { - command.enqueue(move); - } - } // Ends loop through CurrentTotalSimplices - - // Do the moves - command.execute(); - - // Update attempted and failed moves - this->m_attempted_moves += command.get_attempted(); - this->m_succeeded_moves += command.get_succeeded(); - this->m_failed_moves += command.get_failed(); - command.reset_counters(); + auto const move = move_tracker::generate_random_move_3(m_generator); + static_cast( + attempt_transition(current, move, acceptance_draw(m_generator))); + } - // Do stuff on checkpoint if (pass_number % m_checkpoint == 0) { - fmt::print("=== Pass {} ===\n", pass_number); print_results(); if (m_write_files) { fmt::print("Writing to file.\n"); - utilities::write_file(command.get_results()); + utilities::write_file(current, m_generator.seed(), pass_number); } } - } // Ends loop through m_passes + } - // output results fmt::print("=== Run results ===\n"); print_results(); - return command.get_results(); + return current; } // operator() /// @brief Display results of run @@ -435,41 +434,41 @@ class MoveStrategy m_proposed_moves.total(), m_accepted_moves.total(), m_rejected_moves.total()); fmt::print( - "There were {} attempted moves with {} successful moves and {} " - "failed moves.\n", + "There were {} candidate construction attempts with {} successful " + "candidates and {} failed candidates.\n", m_attempted_moves.total(), m_succeeded_moves.total(), m_failed_moves.total()); fmt::print( - "(2,3) moves: {} proposed ({} accepted and {} rejected) with {} " - "attempted ({} successful and {} failed).\n", + "(2,3) moves: {} proposed ({} accepted and {} rejected); candidate " + "construction: {} attempted ({} succeeded and {} failed).\n", m_proposed_moves.two_three_moves(), m_accepted_moves.two_three_moves(), m_rejected_moves.two_three_moves(), m_attempted_moves.two_three_moves(), m_succeeded_moves.two_three_moves(), m_failed_moves.two_three_moves()); fmt::print( - "(3,2) moves: {} proposed ({} accepted and {} rejected) with {} " - "attempted ({} successful and {} failed).\n", + "(3,2) moves: {} proposed ({} accepted and {} rejected); candidate " + "construction: {} attempted ({} succeeded and {} failed).\n", m_proposed_moves.three_two_moves(), m_accepted_moves.three_two_moves(), m_rejected_moves.three_two_moves(), m_attempted_moves.three_two_moves(), m_succeeded_moves.three_two_moves(), m_failed_moves.three_two_moves()); fmt::print( - "(2,6) moves: {} proposed ({} accepted and {} rejected) with {} " - "attempted ({} successful and {} failed).\n", + "(2,6) moves: {} proposed ({} accepted and {} rejected); candidate " + "construction: {} attempted ({} succeeded and {} failed).\n", m_proposed_moves.two_six_moves(), m_accepted_moves.two_six_moves(), m_rejected_moves.two_six_moves(), m_attempted_moves.two_six_moves(), m_succeeded_moves.two_six_moves(), m_failed_moves.two_six_moves()); fmt::print( - "(6,2) moves: {} proposed ({} accepted and {} rejected) with {} " - "attempted ({} successful and {} failed).\n", + "(6,2) moves: {} proposed ({} accepted and {} rejected); candidate " + "construction: {} attempted ({} succeeded and {} failed).\n", m_proposed_moves.six_two_moves(), m_accepted_moves.six_two_moves(), m_rejected_moves.six_two_moves(), m_attempted_moves.six_two_moves(), m_succeeded_moves.six_two_moves(), m_failed_moves.six_two_moves()); fmt::print( - "(4,4) moves: {} proposed ({} accepted and {} rejected) with {} " - "attempted ({} successful and {} failed).\n", + "(4,4) moves: {} proposed ({} accepted and {} rejected); candidate " + "construction: {} attempted ({} succeeded and {} failed).\n", m_proposed_moves.four_four_moves(), m_accepted_moves.four_four_moves(), m_rejected_moves.four_four_moves(), m_attempted_moves.four_four_moves(), m_succeeded_moves.four_four_moves(), m_failed_moves.four_four_moves()); diff --git a/include/Move_always.hpp b/include/Move_always.hpp index 99fdb2f303..328c839dec 100644 --- a/include/Move_always.hpp +++ b/include/Move_always.hpp @@ -17,6 +17,7 @@ #include "Move_command.hpp" #include "Move_strategy.hpp" +#include "Random.hpp" /// @brief The Move Always algorithm template @@ -35,6 +36,9 @@ class MoveStrategy // NOLINT /// triangulation. Int_precision m_checkpoint{1}; + /// @brief Run-owned random stream used for move selection and site ordering + cdt::Random m_random; + /// @brief The number of moves that were attempted by a MoveCommand Counter m_attempted_moves; @@ -54,7 +58,23 @@ class MoveStrategy // NOLINT /// @param t_checkpoint Number of passes per checkpoint [[maybe_unused]] MoveStrategy(Int_precision const t_number_of_passes, Int_precision const t_checkpoint) - : m_passes{t_number_of_passes}, m_checkpoint{t_checkpoint} + : MoveStrategy{t_number_of_passes, t_checkpoint, cdt::Random{}} + {} + + /// @brief Construct a replayable MoveAlways run from an explicit seed. + [[maybe_unused]] MoveStrategy(Int_precision const t_number_of_passes, + Int_precision const t_checkpoint, + cdt::Random_seed const seed) + : MoveStrategy{t_number_of_passes, t_checkpoint, cdt::Random{seed}} + {} + + /// @brief Construct a MoveAlways run from an owned PCG stream. + [[maybe_unused]] MoveStrategy(Int_precision const t_number_of_passes, + Int_precision const t_checkpoint, + cdt::Random random) + : m_passes{t_number_of_passes} + , m_checkpoint{t_checkpoint} + , m_random{std::move(random)} { if (m_passes < 0) { @@ -73,6 +93,9 @@ class MoveStrategy // NOLINT /// @returns The number of passes per checkpoint [[nodiscard]] auto checkpoint() const { return m_checkpoint; } + /// @returns The effective root seed used for this run. + [[nodiscard]] auto seed() const noexcept { return m_random.seed(); } + /// @returns The MoveTracker of attempted moves auto get_attempted() const { return m_attempted_moves; } @@ -90,6 +113,8 @@ class MoveStrategy // NOLINT #endif fmt::print("Starting Move Always algorithm in {}+1 dimensions ...\n", ManifoldType::dimension - 1); + fmt::print("Effective random seed: {} (stream {}).\n", m_random.seed(), + m_random.stream()); m_attempted_moves.reset(); m_successful_moves.reset(); @@ -110,14 +135,9 @@ class MoveStrategy // NOLINT ++move_attempt) { // Pick a move to attempt - auto move_choice = utilities::generate_random_int( - 0, move_tracker::NUMBER_OF_3D_MOVES - 1); -#ifndef NDEBUG - fmt::print("Move choice = {}\n", move_choice); -#endif - command.enqueue(move_tracker::as_move(move_choice)); + command.enqueue(move_tracker::generate_random_move_3(m_random)); } - command.execute(); + command.execute(m_random); // Update attempted, successful, and failed moves m_attempted_moves += command.get_attempted(); m_successful_moves += command.get_succeeded(); @@ -128,7 +148,8 @@ class MoveStrategy // NOLINT { fmt::print("Writing checkpoint for pass {}.\n", pass_number); print_results(); - utilities::write_file(command.get_results()); + utilities::write_file(command.get_results(), m_random.seed(), + pass_number); } } print_results(); diff --git a/include/Move_command.hpp b/include/Move_command.hpp index 76e49cb9eb..69dd92b1e7 100644 --- a/include/Move_command.hpp +++ b/include/Move_command.hpp @@ -11,13 +11,13 @@ #ifndef CDT_PLUSPLUS_MOVECOMMAND_HPP #define CDT_PLUSPLUS_MOVECOMMAND_HPP -#include "Apply_move.hpp" +#include + #include "Ergodic_moves_3.hpp" +#include "Random.hpp" template , - typename FunctionType = - boost::compat::function_ref> + typename ExpectedType = std::expected> requires(ManifoldType::dimension == 3) class MoveCommand { @@ -120,71 +120,58 @@ class MoveCommand /** * \brief Execute all moves in the queue on the manifold */ - void execute() + template + void execute(Generator& generator) { -#ifndef NDEBUG - fmt::print("=== Executing: Before moves ===\n"); - m_manifold.print_details(); - fmt::print("===============================\n"); -#endif - while (!m_moves.empty()) { auto move_type = m_moves.back(); // Record attempted move ++m_attempted[as_integer(move_type)]; - // Convert move_type to function - auto move_function = as_move_function(move_type); - if (auto result = apply_move(std::as_const(m_manifold), move_function); + if (auto result = apply_random_move(std::as_const(m_manifold), move_type, + generator); result) { - if (result->is_correct()) + if (result->is_correct() && + ergodic_moves::check_move(m_manifold, *result, move_type)) { swap(result.value(), m_manifold); ++m_succeeded[as_integer(move_type)]; } else { - fmt::print("Move produced an invalid manifold.\n"); + spdlog::warn( + "Move violated a manifold invariant or geometry delta.\n"); ++m_failed[as_integer(move_type)]; } } else { - fmt::print("{}\n", result.error()); - // Track failed moves + // Routine inapplicable sites are represented by the failed counter. ++m_failed[as_integer(move_type)]; } // Remove move from queue m_moves.pop_back(); } -#ifndef NDEBUG - fmt::print("=== After moves ===\n"); - print_attempts(); - print_successful(); - print_errors(); - m_manifold.print_details(); - fmt::print("===================\n"); -#endif } // execute - /** - * \brief Execute a move function on a manifold - * \param move The move to execute - * \return The move function to execute - */ - static auto as_move_function(move_tracker::move_type const move) - -> FunctionType + /// @brief Apply one queued move using the caller-owned random stream. + template + static auto apply_random_move(ManifoldType const& manifold, + move_tracker::move_type const move, + Generator& generator) -> ExpectedType { + using enum move_tracker::move_type; switch (move) { - case move_tracker::move_type::TWO_THREE: return ergodic_moves::do_23_move; - case move_tracker::move_type::THREE_TWO: return ergodic_moves::do_32_move; - case move_tracker::move_type::TWO_SIX: return ergodic_moves::do_26_move; - case move_tracker::move_type::SIX_TWO: return ergodic_moves::do_62_move; - default: return ergodic_moves::do_44_move; + case TWO_THREE: return ergodic_moves::do_23_move(manifold, generator); + case THREE_TWO: return ergodic_moves::do_32_move(manifold, generator); + case TWO_SIX: return ergodic_moves::do_26_move(manifold, generator); + case SIX_TWO: return ergodic_moves::do_62_move(manifold, generator); + case FOUR_FOUR: return ergodic_moves::do_44_move(manifold, generator); } - } // move_function + return std::unexpected("Unknown Pachner move."); + } /** * \brief Print attempted moves diff --git a/include/Move_tracker.hpp b/include/Move_tracker.hpp index 88f6795c04..efc93da909 100644 --- a/include/Move_tracker.hpp +++ b/include/Move_tracker.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include "Settings.hpp" @@ -62,16 +63,14 @@ namespace move_tracker return move_type::FOUR_FOUR; } // as_move - /** - * \brief Generate random 3D ergodic move - * \return The move_type to be performed - */ - [[nodiscard]] inline auto generate_random_move_3() -> move_type + /// Generate a uniformly distributed 3D ergodic move from caller-owned RNG. + template + [[nodiscard]] inline auto generate_random_move_3(Generator& generator) + -> move_type { - auto move_choice = utilities::generate_random_int(0, 4); -#ifndef NDEBUG - fmt::print("Move choice = {}\n", move_choice); -#endif + std::uniform_int_distribution distribution{ + 0, static_cast(NUMBER_OF_3D_MOVES - 1)}; + auto const move_choice = distribution(generator); return as_move(move_choice); } // generate_random_move_3 diff --git a/include/Mpfr_value.hpp b/include/Mpfr_value.hpp index d7757e0bbc..c0aaabc2b8 100644 --- a/include/Mpfr_value.hpp +++ b/include/Mpfr_value.hpp @@ -19,7 +19,14 @@ namespace mpfr_values { - using Value = CGAL::Gmpfr; + using Value = CGAL::Gmpfr; + + /// MPFR round-to-nearest with ties to an even significand. + /// + /// Keeping one explicit policy at every arithmetic boundary prevents a + /// caller from accidentally mixing directed roundings inside an action + /// calculation. + static inline auto constexpr rounding_mode = MPFR_RNDN; static_assert(std::is_nothrow_destructible_v, "MPFR values must release their resources without throwing."); @@ -32,14 +39,14 @@ namespace mpfr_values [[nodiscard]] inline auto from_integer(long const value) -> Value { auto result = zero(); - mpfr_set_si(result.fr(), value, MPFR_RNDD); + mpfr_set_si(result.fr(), value, rounding_mode); return result; } [[nodiscard]] inline auto from_long_double(long double const value) -> Value { auto result = zero(); - mpfr_set_ld(result.fr(), value, MPFR_RNDD); + mpfr_set_ld(result.fr(), value, rounding_mode); return result; } @@ -50,7 +57,7 @@ namespace mpfr_values throw std::invalid_argument("MPFR decimal value must not be null."); } auto result = zero(); - if (mpfr_set_str(result.fr(), value, 10, MPFR_RNDD) != 0) + if (mpfr_set_str(result.fr(), value, 10, rounding_mode) != 0) { throw std::invalid_argument("Invalid decimal MPFR value."); } @@ -60,14 +67,14 @@ namespace mpfr_values [[nodiscard]] inline auto pi() -> Value { auto result = zero(); - mpfr_const_pi(result.fr(), MPFR_RNDD); + mpfr_const_pi(result.fr(), rounding_mode); return result; } [[nodiscard]] inline auto add(Value const& left, Value const& right) -> Value { auto result = zero(); - mpfr_add(result.fr(), left.fr(), right.fr(), MPFR_RNDD); + mpfr_add(result.fr(), left.fr(), right.fr(), rounding_mode); return result; } @@ -75,7 +82,7 @@ namespace mpfr_values -> Value { auto result = zero(); - mpfr_sub(result.fr(), left.fr(), right.fr(), MPFR_RNDD); + mpfr_sub(result.fr(), left.fr(), right.fr(), rounding_mode); return result; } @@ -83,7 +90,7 @@ namespace mpfr_values -> Value { auto result = zero(); - mpfr_mul(result.fr(), left.fr(), right.fr(), MPFR_RNDD); + mpfr_mul(result.fr(), left.fr(), right.fr(), rounding_mode); return result; } @@ -91,50 +98,50 @@ namespace mpfr_values Value const& denominator) -> Value { auto result = zero(); - mpfr_div(result.fr(), numerator.fr(), denominator.fr(), MPFR_RNDD); + mpfr_div(result.fr(), numerator.fr(), denominator.fr(), rounding_mode); return result; } [[nodiscard]] inline auto square_root(Value const& value) -> Value { auto result = zero(); - mpfr_sqrt(result.fr(), value.fr(), MPFR_RNDD); + mpfr_sqrt(result.fr(), value.fr(), rounding_mode); return result; } [[nodiscard]] inline auto inverse_hyperbolic_sine(Value const& value) -> Value { auto result = zero(); - mpfr_asinh(result.fr(), value.fr(), MPFR_RNDD); + mpfr_asinh(result.fr(), value.fr(), rounding_mode); return result; } [[nodiscard]] inline auto arc_cosine(Value const& value) -> Value { auto result = zero(); - mpfr_acos(result.fr(), value.fr(), MPFR_RNDD); + mpfr_acos(result.fr(), value.fr(), rounding_mode); return result; } [[nodiscard]] inline auto negate(Value const& value) -> Value { auto result = zero(); - mpfr_neg(result.fr(), value.fr(), MPFR_RNDD); + mpfr_neg(result.fr(), value.fr(), rounding_mode); return result; } [[nodiscard]] inline auto exponential(Value const& value) -> Value { auto result = zero(); - mpfr_exp(result.fr(), value.fr(), MPFR_RNDD); + mpfr_exp(result.fr(), value.fr(), rounding_mode); return result; } [[nodiscard]] inline auto to_double(Value const& value) -> double - { return mpfr_get_d(value.fr(), MPFR_RNDD); } + { return mpfr_get_d(value.fr(), rounding_mode); } [[nodiscard]] inline auto to_long_double(Value const& value) -> long double - { return mpfr_get_ld(value.fr(), MPFR_RNDD); } + { return mpfr_get_ld(value.fr(), rounding_mode); } } // namespace mpfr_values #endif // CDT_PLUSPLUS_MPFR_VALUE_HPP diff --git a/include/Random.hpp b/include/Random.hpp new file mode 100644 index 0000000000..73b33550ff --- /dev/null +++ b/include/Random.hpp @@ -0,0 +1,96 @@ +/******************************************************************************* + Causal Dynamical Triangulations in C++ using CGAL + + Copyright © 2026 Adam Getchell + ******************************************************************************/ + +/// @file Random.hpp +/// @brief Run-owned random-number generation and reproducible stream splitting + +#ifndef CDT_PLUSPLUS_RANDOM_HPP +#define CDT_PLUSPLUS_RANDOM_HPP + +#include +#include +#include +#include + +#include "pcg_random.hpp" + +namespace cdt +{ + using Random_seed = std::uint64_t; + using Random_stream = std::uint64_t; + + namespace random_streams + { + inline Random_stream constexpr initialization{0}; + inline Random_stream constexpr transitions{1}; + } // namespace random_streams + + /// @brief A run-owned PCG engine with a recorded seed and stream identifier. + /// @details Construct one root engine per simulation. Pass engines by + /// reference to distributions and stochastic algorithms instead of drawing + /// fresh entropy for each sample. `split()` creates a reproducible, + /// independently parameterized PCG stream for a subsystem or worker. + /// + /// Random is intentionally not internally synchronized. A mutable instance + /// belongs to one run or one thread. Parallel code must give each worker a + /// distinct stream before drawing from it. + /// @see [PCG random-number + /// generators](../REFERENCES.md#pcg-random-number-generators) + class Random final + { + public: + using result_type = pcg64::result_type; + + private: + Random_seed m_seed{}; + Random_stream m_stream{}; + pcg64 m_engine; + + [[nodiscard]] static auto entropy_seed() -> Random_seed + { + std::random_device entropy; + std::uniform_int_distribution distribution{ + std::numeric_limits::min(), + std::numeric_limits::max()}; + return distribution(entropy); + } + + public: + /// @brief Construct a root stream from operating-system entropy. + Random() : Random{entropy_seed()} {} + + /// @brief Construct a reproducible PCG stream without consulting entropy. + /// @param seed Root seed recorded for the run. + /// @param stream PCG stream selector; distinct values select distinct + /// sequences for the same root seed. + explicit Random(Random_seed const seed, Random_stream const stream = 0) + : m_seed{seed}, m_stream{stream}, m_engine{seed, stream} + {} + + [[nodiscard]] static auto constexpr min() noexcept -> result_type + { return pcg64::min(); } + + [[nodiscard]] static auto constexpr max() noexcept -> result_type + { return pcg64::max(); } + + [[nodiscard]] auto operator()() -> result_type { return m_engine(); } + + /// @returns The effective root seed for replaying this run. + [[nodiscard]] auto seed() const noexcept -> Random_seed { return m_seed; } + + /// @returns The PCG stream selector used by this engine. + [[nodiscard]] auto stream() const noexcept -> Random_stream + { return m_stream; } + + /// @brief Create a fresh reproducible stream from the same root seed. + [[nodiscard]] auto split(Random_stream const stream) const -> Random + { return Random{m_seed, stream}; } + }; + + static_assert(std::uniform_random_bit_generator); +} // namespace cdt + +#endif // CDT_PLUSPLUS_RANDOM_HPP diff --git a/include/S3Action.hpp b/include/S3Action.hpp index d5f614349a..1f4c0a7220 100644 --- a/include/S3Action.hpp +++ b/include/S3Action.hpp @@ -13,6 +13,8 @@ /// Note: for performance reasons, variables should not hold successively /// increasing values. We avoid this by setting each variable only once. /// See https://gmplib.org/manual/Efficiency.html#Efficiency for details. +/// @see [Regge calculus](../REFERENCES.md#regge-calculus) +/// @see [Three-dimensional CDT](../REFERENCES.md#three-dimensional-cdt-2001) #ifndef INCLUDE_S3ACTION_HPP_ #define INCLUDE_S3ACTION_HPP_ @@ -80,13 +82,11 @@ namespace s3_action /// @param K \f$k=\frac{1}{8\pi G_{Newton}}\f$ /// @param Lambda \f$\lambda=k*\Lambda\f$ where \f$\Lambda\f$ is the /// Cosmological constant -/// @returns \f$S^{(3)}(\alpha=-1)\f$ as a -/// Gmpzf -/// value +/// @returns \f$S^{(3)}(\alpha=-1)\f$ as a 256-bit MPFR value [[nodiscard]] inline auto S3_bulk_action_alpha_minus_one( Int_precision const N1_TL, Int_precision const N3_31_13, Int_precision const N3_22, long double const K, long double const Lambda) - -> Gmpzf + -> mpfr_values::Value { auto const [checked_k, checked_lambda] = s3_action::make_finite_couplings(K, Lambda); @@ -124,9 +124,7 @@ namespace s3_action auto const r12 = mpfr_values::subtract(r7, r3); // r12 = r7-r3 auto const total = mpfr_values::add(r11, r12); // total = r11+r12 - // Convert MPFR total to Gmpzf result by using Gmpzf(double d) - // Perhaps fixable later by switching to MP_Float - return Gmpzf{mpfr_values::to_double(total)}; + return total; } // S3_bulk_action_alpha_minus_one() /// @brief Calculates S3 bulk action for \f$\alpha\f$=1. @@ -143,15 +141,13 @@ namespace s3_action /// @param K \f$k=\frac{1}{8\pi G_{Newton}}\f$ /// @param Lambda \f$\lambda=k*\Lambda\f$ where \f$\Lambda\f$ is the /// Cosmological constant -/// @returns \f$S^{(3)}(\alpha=1)\f$ as a -/// Gmpzf -/// value +/// @returns \f$S^{(3)}(\alpha=1)\f$ as a 256-bit MPFR value [[nodiscard]] inline auto S3_bulk_action_alpha_one(Int_precision const N1_TL, Int_precision const N3_31_13, Int_precision const N3_22, long double const K, long double const Lambda) - -> Gmpzf + -> mpfr_values::Value { auto const [checked_k, checked_lambda] = s3_action::make_finite_couplings(K, Lambda); @@ -192,10 +188,8 @@ namespace s3_action auto const r12 = mpfr_values::add(r3, r7); // r12 = r3+r7 auto const total = mpfr_values::add(r11, r12); // total = r11+r12 - // Convert MPFR total to Gmpzf result by using Gmpzf(double d) - // Perhaps fixable later by switching to MP_Float - return Gmpzf{mpfr_values::to_double(total)}; -} // Gmpzf S3_bulk_action_alpha_one() + return total; +} // S3_bulk_action_alpha_one() /// @brief Calculates the generalized S3 bulk action in terms of \f$\alpha\f$, /// \f$k\f$, \f$\lambda\f$, \f$N_1^{TL}\f$, \f$N_3^{(3,1)}\f$, and @@ -220,15 +214,14 @@ namespace s3_action /// @param K \f$k=\frac{1}{8\pi G_{Newton}}\f$ /// @param Lambda \f$\lambda=k*\Lambda\f$ where \f$\Lambda\f$ is the /// Cosmological constant -/// @returns \f$S^{(3)}(\alpha)\f$ as a -/// Gmpzf -/// value -[[nodiscard]] inline auto S3_bulk_action(Int_precision const N1_TL, - Int_precision const N3_31_13, - Int_precision const N3_22, - long double const Alpha, - long double const K, - long double const Lambda) -> Gmpzf +/// @returns \f$S^{(3)}(\alpha)\f$ as a 256-bit MPFR value +/// @see [Regge calculus](../REFERENCES.md#regge-calculus) +/// @see [Three-dimensional CDT +/// action](../REFERENCES.md#three-dimensional-cdt-2001) +[[nodiscard]] inline auto S3_bulk_action( + Int_precision const N1_TL, Int_precision const N3_31_13, + Int_precision const N3_22, long double const Alpha, long double const K, + long double const Lambda) -> mpfr_values::Value { auto const parameters = s3_action::make_physical_parameters(Alpha, K, Lambda); @@ -330,10 +323,8 @@ namespace s3_action auto const r52 = mpfr_values::add(r5, r30); // r52 = r5+r30 auto const total = mpfr_values::add(r51, r52); // total = r51+r52 - // Convert MPFR total to Gmpzf result by using Gmpzf(double d) - // Perhaps fixable later by switching to MP_Float - return Gmpzf{mpfr_values::to_double(total)}; -} // Gmpzf S3_bulk_action() + return total; +} // S3_bulk_action() #pragma GCC diagnostic pop diff --git a/include/Utilities.hpp b/include/Utilities.hpp index c1be346d08..f39ca2ee46 100644 --- a/include/Utilities.hpp +++ b/include/Utilities.hpp @@ -33,9 +33,6 @@ /// clang-15 does not support std::format // #include -// M. O'Neill Permutation Congruential Generator library -#include "pcg_random.hpp" - // V. Zverovich {fmt} library #include @@ -45,6 +42,7 @@ #include // Global project settings +#include "Random.hpp" #include "Settings.hpp" enum class topology_type @@ -205,6 +203,29 @@ namespace utilities manifold.foliation_spacing()); } // make_filename + /// @brief Generate a filename that records the effective run seed. + template + [[nodiscard]] auto make_filename(ManifoldType const& manifold, + cdt::Random_seed const seed) + { + auto const base = make_filename(manifold); + return base.parent_path() / + (base.stem().string() + "-seed-" + std::to_string(seed) + + base.extension().string()); + } + + /// @brief Generate a checkpoint filename that records seed and pass. + template + [[nodiscard]] auto make_filename(ManifoldType const& manifold, + cdt::Random_seed const seed, + Int_precision const completed_passes) + { + auto const base = make_filename(manifold, seed); + return base.parent_path() / + (base.stem().string() + "-pass-" + std::to_string(completed_passes) + + base.extension().string()); + } + /// @brief Print triangulation statistics /// @tparam TriangulationType The triangulation type /// @param t_triangulation A triangulation (typically a Delaunay_t<3> @@ -293,6 +314,22 @@ namespace utilities write_file(filename, t_universe.delaunay_snapshot()); } // write_file + /// @brief Write a triangulation with the effective seed in its filename. + template + void write_file(ManifoldType const& t_universe, cdt::Random_seed const seed) + { + write_file(make_filename(t_universe, seed), t_universe.delaunay_snapshot()); + } + + /// @brief Write a checkpoint with its effective seed and pass in its name. + template + void write_file(ManifoldType const& t_universe, cdt::Random_seed const seed, + Int_precision const completed_passes) + { + write_file(make_filename(t_universe, seed, completed_passes), + t_universe.delaunay_snapshot()); + } + /// @brief Read triangulation from file /// @tparam TriangulationType The type of triangulation /// @param filename The file to read from @@ -329,25 +366,21 @@ namespace utilities } // read_file /// @brief Roll a die with PCG - [[nodiscard]] inline auto die_roll() + template + [[nodiscard]] inline auto die_roll(Generator& generator) { - pcg_extras::seed_seq_from seed_source; - - // Make a random number generator - pcg64 rng(seed_source); - // Choose random number from 1 to 6 std::uniform_int_distribution uniform_dist(1, 6); // NOLINT - Int_precision const roll = uniform_dist(rng); + Int_precision const roll = uniform_dist(generator); return roll; } // die_roll() /// @brief Generate random numbers /// /// Uses Melissa E. O'Neill's Permuted Congruential Generator for high-quality - /// RNG which passes the TestU01 statistical tests. See: - /// http://www.pcg-random.org/paper.html - /// for more details + /// RNG which passes the TestU01 statistical tests. + /// @see [PCG random-number + /// generators](../REFERENCES.md#pcg-random-number-generators) /// /// @tparam NumberType The type of number in the RNG /// @tparam Distribution The distribution type, usually uniform @@ -355,62 +388,58 @@ namespace utilities /// @param t_max_value The maximum value /// @returns A random value in the distribution between min_value and /// max_value - template - [[nodiscard]] auto generate_random(NumberType t_min_value, + template + [[nodiscard]] auto generate_random(Generator& generator, + NumberType t_min_value, NumberType t_max_value) { - pcg_extras::seed_seq_from seed_source; - // Make a random number generator - pcg64 generator(seed_source); Distribution distribution(t_min_value, t_max_value); return distribution(generator); } // generate_random() - /// @brief Make a high-quality random number generator usable by std::shuffle - /// @returns A RNG - inline auto make_random_generator() - { - pcg_extras::seed_seq_from seed_source; - pcg64 generator(seed_source); - return generator; - } // make_random_generator() - /// @brief Generate random integers by calling generate_random, preserves /// template argument deduction - template - [[nodiscard]] auto generate_random_int(IntegerType t_min_value, + template + [[nodiscard]] auto generate_random_int(Generator& generator, + IntegerType t_min_value, IntegerType t_max_value) { using int_dist = std::uniform_int_distribution; - return generate_random(t_min_value, t_max_value); + return generate_random(generator, t_min_value, + t_max_value); } // generate_random_int() /// @brief Generate a random timeslice - template - [[nodiscard]] auto generate_random_timeslice(IntegerType&& t_max_timeslice) + template + [[nodiscard]] auto generate_random_timeslice(Generator& generator, + IntegerType&& t_max_timeslice) -> decltype(auto) { - return generate_random_int(static_cast(1), + return generate_random_int(generator, static_cast(1), std::forward(t_max_timeslice)); } // generate_random_timeslice() /// @brief Generate random real numbers by calling generate_random, preserves /// template argument deduction - template - [[nodiscard]] auto generate_random_real(FloatingPointType t_min_value, + template + [[nodiscard]] auto generate_random_real(Generator& generator, + FloatingPointType t_min_value, FloatingPointType t_max_value) { using real_dist = std::uniform_real_distribution; - return generate_random(t_min_value, + return generate_random(generator, t_min_value, t_max_value); } // generate_random_real() /// @brief Generate a probability - [[nodiscard]] inline auto generate_probability() + template + [[nodiscard]] inline auto generate_probability(Generator& generator) { auto constexpr min = 0.0L; auto constexpr max = 1.0L; - return generate_random_real(min, max); + return generate_random_real(generator, min, max); } // generate_probability() /// @brief Calculate expected # of points per simplex diff --git a/pyproject.toml b/pyproject.toml index ebbdd27cb7..2a8abc0f1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,16 +1,44 @@ [project] name = "cdt-plusplus-scripts" -version = "0.0.0" +# PEP 440 spelling of the CDT++ v1.0.0-rc1 release. +version = "1.0.0rc1" description = "Python support and experiment scripts for CDT++" requires-python = ">=3.12,<3.13" dependencies = [] +[project.scripts] +cdt-mnist-experiment = "scripts.mnist_experiment:main" +cdt-optimize-initialize = "scripts.optimize_initialize:main" + +[build-system] +requires = ["uv_build>=0.11.29,<0.12"] +build-backend = "uv_build" + [tool.uv] default-groups = ["dev"] -package = false +package = true + +[tool.uv.build-backend] +module-name = "scripts" +module-root = "" +source-exclude = [ + "scripts/*.bat", + "scripts/*.sh", + "scripts/release_check.py", + "scripts/semgrep_fixture_config.py", + "scripts/tests/**", +] [dependency-groups] -dev = ["ruff==0.15.21", "ty==0.0.59"] +dev = [ + "actionlint-py==1.7.12.24", + "clang-format==22.1.8", + "pyyaml==6.0.3", + "ruff==0.15.21", + "semgrep==1.169.0", + "ty==0.0.59", + "yamllint==1.38.0", +] experiments = [ "comet-ml>=3.58.3,<4", "matplotlib>=3.10,<4", @@ -20,7 +48,7 @@ experiments = [ [tool.ruff] line-length = 160 -src = ["src"] +src = ["scripts"] target-version = "py312" [tool.ruff.lint] @@ -103,8 +131,10 @@ ignore = [ ] [tool.ruff.lint.per-file-ignores] -"src/*.py" = ["INP001"] - +"scripts/tests/test_*.py" = [ + "PT009", # The dependency-free support-script suite intentionally uses unittest. + "PT027", # The dependency-free support-script suite intentionally uses unittest. +] [tool.ruff.lint.mccabe] max-complexity = 10 diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000000..9374ca7579 --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1 @@ +"""CDT++ Python support and experiment scripts.""" diff --git a/scripts/clang-tidy.sh b/scripts/clang-tidy.sh index 517fce7b56..1912893db2 100755 --- a/scripts/clang-tidy.sh +++ b/scripts/clang-tidy.sh @@ -8,19 +8,17 @@ set -euo pipefail script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd -- "${script_dir}/.." && pwd)" build_dir="${repo_root}/build" -if command -v just >/dev/null; then - llvm_version="$(just --justfile "${repo_root}/Justfile" --evaluate llvm_version)" -elif command -v pkgx >/dev/null; then - llvm_version="$(pkgx +just.systems -- just --justfile "${repo_root}/Justfile" --evaluate llvm_version)" -else - echo "just is required to resolve the pinned LLVM version; install it or install pkgx." >&2 +if ! command -v just >/dev/null; then + echo "just is required to resolve repository tool versions; run this through 'just clang-tidy'." >&2 exit 1 fi +just_version="$(just --justfile "${repo_root}/Justfile" --evaluate just_version)" +llvm_version="$(just --justfile "${repo_root}/Justfile" --evaluate llvm_version)" clang_tidy_vcpkg_installed_dir="${repo_root}/.cache/vcpkg-installed/clang-tidy-llvm-${llvm_version}" if [[ "${CDT_CLANG_TIDY_ACTIVE:-0}" != 1 ]] && command -v pkgx >/dev/null; then export CDT_CLANG_TIDY_ACTIVE=1 - exec pkgx +just.systems "+llvm.org@${llvm_version}" +cmake.org +ninja-build.org -- "${BASH_SOURCE[0]}" "$@" + exec pkgx "+just.systems@${just_version}" "+llvm.org@${llvm_version}" +cmake.org +ninja-build.org -- "${BASH_SOURCE[0]}" "$@" fi command -v clang-tidy >/dev/null || { diff --git a/scripts/mnist_experiment.py b/scripts/mnist_experiment.py new file mode 100644 index 0000000000..9a66f18040 --- /dev/null +++ b/scripts/mnist_experiment.py @@ -0,0 +1,64 @@ +"""Run the CDT++ TensorFlow MNIST experiment.""" + +from __future__ import annotations + +import argparse +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Sequence + + +def _parse_args(argv: Sequence[str]) -> argparse.Namespace: + """Parse command-line arguments without loading TensorFlow.""" + parser = argparse.ArgumentParser(description=__doc__) + return parser.parse_args(argv) + + +def _run_experiment() -> None: + """Train and evaluate the historical TensorFlow model.""" + import tensorflow as tf # noqa: PLC0415 + + tf.keras.utils.set_random_seed(0) + mnist = tf.keras.datasets.mnist + (x_train, y_train), (x_test, y_test) = mnist.load_data() + x_train, x_test = x_train / 255.0, x_test / 255.0 + + model = tf.keras.models.Sequential( + [ + tf.keras.layers.Flatten(input_shape=(28, 28)), + tf.keras.layers.Dense(128, activation="relu"), + tf.keras.layers.Dropout(0.2), + tf.keras.layers.Dense(10), + ] + ) + predictions = model(x_train[:1]).numpy() + tf.nn.softmax(predictions).numpy() + + loss_function = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True) + loss_function(y_train[:1], predictions).numpy() + model.compile(optimizer="adam", loss=loss_function, metrics=["accuracy"]) + model.fit(x_train, y_train, epochs=5) + model.evaluate(x_test, y_test, verbose=2) + + probability_model = tf.keras.Sequential([model, tf.keras.layers.Softmax()]) + probability_model(x_test[:5]) + + +def main(argv: Sequence[str] | None = None) -> int: + """Run the MNIST experiment from an installed uv entry point.""" + _parse_args(sys.argv[1:] if argv is None else argv) + try: + _run_experiment() + except ModuleNotFoundError as error: + print( + f"Missing experiment dependency {error.name!r}; run with `uv run --group experiments cdt-mnist-experiment`.", + file=sys.stderr, + ) + return 2 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/optimize_initialize.py b/scripts/optimize_initialize.py new file mode 100644 index 0000000000..df982cb7a6 --- /dev/null +++ b/scripts/optimize_initialize.py @@ -0,0 +1,151 @@ +"""Run the CDT++ initializer parameter optimization experiment.""" + +from __future__ import annotations + +import argparse +import os +import re +import sys +from pathlib import Path +from subprocess import check_output as qx +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Sequence + + +def _parse_args(argv: Sequence[str]) -> argparse.Namespace: + """Parse command-line arguments without loading experiment dependencies.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--repository-root", + type=Path, + default=Path.cwd(), + help="CDT++ checkout containing out/build/reference (default: current directory)", + ) + return parser.parse_args(argv) + + +def _parse_initializer_output(output: str) -> tuple[int, list[tuple[int, int]]]: + """Extract the final size and volume profile from initializer output.""" + final_simplices = None + graph: list[tuple[int, int]] = [] + for line in output.splitlines(): + if match := re.fullmatch(r"Final number of simplices: (?P\d+)", line): + final_simplices = int(match.group("count")) + elif line.startswith("Timeslice"): + match = re.fullmatch(r"Timeslice (?P\d+) has (?P\d+) spacelike faces[.]", line) + if match: + graph.append((int(match.group("timeslice")), int(match.group("volume")))) + + if final_simplices is None: + message = "Initializer output did not report the final number of simplices." + raise RuntimeError(message) + if not graph: + message = "Initializer output did not contain a timeslice volume profile." + raise RuntimeError(message) + return final_simplices, graph + + +def _initializer_binary(repository_root: Path, platform: str = sys.platform) -> Path: + """Return the reference initializer path for the active operating system.""" + executable = "initialize.exe" if platform == "win32" else "initialize" + return repository_root / "out" / "build" / "reference" / "src" / executable + + +def _run_experiments(initialize_binary: Path, api_key: str) -> None: + """Run the historical Comet parameter sweep.""" + import comet_ml as cm # noqa: PLC0415 + import matplotlib.pyplot as plt # noqa: PLC0415 + import numpy as np # noqa: PLC0415 + from comet_ml import Experiment # noqa: PLC0415 + + parameters = [(initial_radius, spacing) for initial_radius in range(1, 4) for spacing in np.arange(1, 2.5, 0.5)] + + try: + for parameter_pair in parameters: + experiment = Experiment(api_key=api_key, project_name="cdt-plusplus") + hyper_params = {"simplices": 12000, "foliations": 12} + experiment.log_multiple_params(hyper_params) + init_radius = parameter_pair[0] + radial_factor = parameter_pair[1] + + command = [ + str(initialize_binary), + "-s", + "-n", + str(hyper_params["simplices"]), + "-t", + str(hyper_params["foliations"]), + "-i", + str(init_radius), + "-f", + str(radial_factor), + ] + print(command) + + # The executable and numeric parameters are repository-controlled. + output = qx(command, text=True) # noqa: S603 + + final_simplices, graph = _parse_initializer_output(output) + + min_timeslice = min(timeslice for timeslice, _ in graph) + max_timeslice = max(timeslice for timeslice, _ in graph) + result = (final_simplices, min_timeslice, max_timeslice) + + print(result) + print(f"Initial radius is: {init_radius}") + print(f"Radial factor is: {radial_factor}") + for timeslice, volume in graph: + print(f"Timeslice {timeslice} has {volume} spacelike faces.") + print() + + target_simplices = hyper_params["simplices"] + score = ((final_simplices - target_simplices) / target_simplices) * 100 + experiment.log_metric("Error %", score) + experiment.log_other("Min Timeslice", result[1]) + experiment.log_other("Max Timeslice", result[2]) + + timeslices = [timeslice for timeslice, _ in graph] + volumes = [volume for _, volume in graph] + plt.plot(timeslices, volumes) + plt.xlabel("Timeslice") + plt.ylabel("Volume (spacelike faces)") + plt.title("Volume Profile") + plt.grid(visible=True) + experiment.log_figure(figure_name="Volume per Timeslice", figure=plt) + plt.clf() + experiment.end() + except cm.exceptions.NoMoreSuggestionsAvailable: + print("No more suggestions.") + + +def main(argv: Sequence[str] | None = None) -> int: + """Run the parameter sweep from an installed uv entry point.""" + args = _parse_args(sys.argv[1:] if argv is None else argv) + repository_root = args.repository_root.resolve() + initialize_binary = _initializer_binary(repository_root) + if not initialize_binary.is_file(): + print(f"CDT++ initializer not found at {initialize_binary}; run `just build` first.", file=sys.stderr) + return 2 + + api_key = os.environ.get("COMET_API_KEY") + if not api_key: + print("COMET_API_KEY is required for an online Comet experiment.", file=sys.stderr) + return 2 + + try: + _run_experiments(initialize_binary, api_key) + except ModuleNotFoundError as error: + print( + f"Missing experiment dependency {error.name!r}; run with `uv run --group experiments cdt-optimize-initialize`.", + file=sys.stderr, + ) + return 2 + + print("All done with parameter optimization; results are available in Comet.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/release_check.py b/scripts/release_check.py new file mode 100755 index 0000000000..1950848c20 --- /dev/null +++ b/scripts/release_check.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +"""Validate CDT++ release metadata and version synchronization.""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +import tomllib +from datetime import date +from pathlib import Path +from typing import TYPE_CHECKING, TypeGuard + +import yaml + +if TYPE_CHECKING: + from collections.abc import Sequence + +type ParsedObject = dict[str, object] + +SEMVER_RE = re.compile(r"[0-9]+[.][0-9]+[.][0-9]+(?:-rc[0-9]+)?") +PEP440_RE = re.compile(r"(?P[0-9]+[.][0-9]+[.][0-9]+)(?:rc(?P[0-9]+))?") +RC_REFERENCE_RE = re.compile(r"(?[0-9]+[.][0-9]+[.][0-9]+-rc[0-9]+)(?![0-9A-Za-z])") +ACTIVE_RELEASE_DOCS = (Path("README.md"), Path("REFERENCES.md"), Path(".github/CONTRIBUTING.md")) + + +class ReleaseCheckError(ValueError): + """A release metadata file is malformed or inconsistent.""" + + +def _is_parsed_object(value: object) -> TypeGuard[ParsedObject]: + """Return whether *value* is a mapping with string keys.""" + return isinstance(value, dict) and all(isinstance(key, str) for key in value) + + +def _require_object(value: object, context: str) -> ParsedObject: + """Return *value* as a parsed mapping or fail with context.""" + if not _is_parsed_object(value): + message = f"{context} must be a mapping" + raise ReleaseCheckError(message) + return value + + +def _require_string(data: ParsedObject, key: str, context: str) -> str: + """Return one required nonempty string field.""" + value = data.get(key) + if not isinstance(value, str) or not value.strip(): + message = f"{context} must contain a nonempty {key!r} string" + raise ReleaseCheckError(message) + return value + + +def _read_toml(path: Path) -> ParsedObject: + """Read one TOML document as a string-keyed mapping.""" + return _require_object(tomllib.loads(path.read_text(encoding="utf-8")), str(path)) + + +def _single_match(path: Path, pattern: re.Pattern[str], description: str) -> str: + """Return the only named ``value`` match in *path*.""" + matches = [match.group("value") for match in pattern.finditer(path.read_text(encoding="utf-8"))] + if len(matches) != 1: + message = f"{path} must contain exactly one {description}; found {len(matches)}" + raise ReleaseCheckError(message) + return matches[0] + + +def _cmake_version(root: Path) -> str: + """Return the canonical product version declared by CMake.""" + path = root / "CMakeLists.txt" + project_version = _single_match( + path, + re.compile(r"project[(][^)]*?\bVERSION\s+(?P[0-9]+[.][0-9]+[.][0-9]+)\b", re.DOTALL), + "project VERSION", + ) + suffix = _single_match( + path, + re.compile(r'^set[(]CDT_VERSION_SUFFIX\s+"(?P[^"]*)"[)]\s*$', re.MULTILINE), + "CDT_VERSION_SUFFIX", + ) + version = f"{project_version}{suffix}" + if SEMVER_RE.fullmatch(version) is None: + message = f"{path} declares unsupported release version {version!r}" + raise ReleaseCheckError(message) + return version + + +def _pep440_to_product_version(version: str, context: str) -> str: + """Convert the repository's PEP 440 spelling to its product spelling.""" + match = PEP440_RE.fullmatch(version) + if match is None: + message = f"{context} declares unsupported PEP 440 version {version!r}" + raise ReleaseCheckError(message) + rc = match.group("rc") + return f"{match.group('base')}-rc{rc}" if rc is not None else match.group("base") + + +def _pyproject_metadata(root: Path) -> tuple[str, str]: + """Return the Python support project name and product version.""" + path = root / "pyproject.toml" + project = _require_object(_read_toml(path).get("project"), f"{path} [project]") + name = _require_string(project, "name", f"{path} [project]") + version = _require_string(project, "version", f"{path} [project]") + return name, _pep440_to_product_version(version, f"{path} [project]") + + +def _uv_lock_version(root: Path, project_name: str) -> str: + """Return the locked version of the local Python support project.""" + path = root / "uv.lock" + packages = _read_toml(path).get("package") + if not isinstance(packages, list): + message = f"{path} must contain [[package]] entries" + raise ReleaseCheckError(message) + matches: list[ParsedObject] = [] + for index, package in enumerate(packages, start=1): + parsed = _require_object(package, f"{path} [[package]] entry {index}") + source = parsed.get("source") + if parsed.get("name") == project_name and _is_parsed_object(source) and source.get("editable") == ".": + matches.append(parsed) + if len(matches) != 1: + message = f"{path} must contain exactly one editable package named {project_name!r}; found {len(matches)}" + raise ReleaseCheckError(message) + version = _require_string(matches[0], "version", f"{path} package {project_name!r}") + return _pep440_to_product_version(version, f"{path} package {project_name!r}") + + +def _citation_metadata(root: Path) -> tuple[str, date]: + """Validate required CFF software fields and return version/date.""" + path = root / "CITATION.cff" + citation = _require_object(yaml.safe_load(path.read_text(encoding="utf-8")), str(path)) + if _require_string(citation, "cff-version", str(path)) != "1.2.0": + message = f"{path} must use cff-version 1.2.0" + raise ReleaseCheckError(message) + if _require_string(citation, "type", str(path)) != "software": + message = f"{path} must describe software" + raise ReleaseCheckError(message) + for key in ("message", "title", "abstract", "repository-code", "url", "license"): + _require_string(citation, key, str(path)) + authors = citation.get("authors") + if not isinstance(authors, list) or not authors: + message = f"{path} must contain at least one author" + raise ReleaseCheckError(message) + for index, author in enumerate(authors, start=1): + parsed_author = _require_object(author, f"{path} author {index}") + _require_string(parsed_author, "family-names", f"{path} author {index}") + _require_string(parsed_author, "given-names", f"{path} author {index}") + version = _require_string(citation, "version", str(path)) + raw_date = _require_string(citation, "date-released", str(path)) + try: + release_date = date.fromisoformat(raw_date) + except ValueError as error: + message = f"{path} date-released must be an ISO calendar date: {raw_date!r}" + raise ReleaseCheckError(message) from error + return version, release_date + + +def _release_versions(root: Path) -> tuple[dict[str, str], date]: + """Collect every structured release version.""" + vcpkg_path = root / "vcpkg.json" + vcpkg = _require_object(json.loads(vcpkg_path.read_text(encoding="utf-8")), str(vcpkg_path)) + pyproject_name, pyproject_version = _pyproject_metadata(root) + citation_version, release_date = _citation_metadata(root) + versions = { + "CMakeLists.txt product version": _cmake_version(root), + "vcpkg.json version": _require_string(vcpkg, "version", str(vcpkg_path)), + "pyproject.toml version": pyproject_version, + "uv.lock local package version": _uv_lock_version(root, pyproject_name), + "docs/Doxyfile PROJECT_NUMBER": _single_match( + root / "docs/Doxyfile", + re.compile(r"^PROJECT_NUMBER\s*=\s*(?P\S+)\s*$", re.MULTILINE), + "PROJECT_NUMBER", + ), + "CITATION.cff version": citation_version, + } + return versions, release_date + + +def _check_active_release_docs(root: Path, expected: str) -> None: + """Reject stale release-candidate references in active documentation.""" + for relative_path in ACTIVE_RELEASE_DOCS: + path = root / relative_path + references = [match.group("version") for match in RC_REFERENCE_RE.finditer(path.read_text(encoding="utf-8"))] + stale = sorted({version for version in references if version != expected}) + if stale: + message = f"{path} contains stale release-candidate versions: {', '.join(stale)}; expected {expected}" + raise ReleaseCheckError(message) + if "-rc" in expected and expected not in references: + message = f"{path} must reference the current release candidate {expected}" + raise ReleaseCheckError(message) + + +def check_release_metadata(root: Path) -> tuple[str, date]: + """Validate release metadata rooted at *root*.""" + versions, release_date = _release_versions(root) + expected = versions["CMakeLists.txt product version"] + mismatches = {source: version for source, version in versions.items() if version != expected} + if mismatches: + details = "; ".join(f"{source}={version}" for source, version in mismatches.items()) + message = f"release metadata must match {expected} from CMakeLists.txt: {details}" + raise ReleaseCheckError(message) + _check_active_release_docs(root, expected) + return expected, release_date + + +def _parse_args(argv: Sequence[str]) -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("root", nargs="?", type=Path, default=Path.cwd(), help="repository root (default: current directory)") + return parser.parse_args(argv) + + +def main(argv: Sequence[str] | None = None) -> int: + """Validate release metadata and report the synchronized version.""" + args = _parse_args(sys.argv[1:] if argv is None else argv) + try: + version, release_date = check_release_metadata(args.root.resolve()) + except (OSError, ReleaseCheckError, json.JSONDecodeError, tomllib.TOMLDecodeError, yaml.YAMLError) as error: + print(f"Release metadata validation failed: {error}", file=sys.stderr) + return 1 + print(f"Release metadata is synchronized at {version} with release date {release_date.isoformat()}.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/semgrep_fixture_config.py b/scripts/semgrep_fixture_config.py new file mode 100755 index 0000000000..0e79dce337 --- /dev/null +++ b/scripts/semgrep_fixture_config.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Generate a per-fixture Semgrep config from fixture annotations.""" + +import argparse +import re +import sys +from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Sequence + +UTF8 = "utf-8" +RULE_ANNOTATION_RE = re.compile(r"(?:ruleid|ok):\s*([^\n]+)") +RULE_SPLIT_RE = re.compile(r"(?m)^ - id: ") + + +def annotated_rule_ids(fixture_text: str) -> list[str]: + """Return unique CDT++ Semgrep rule IDs referenced by one fixture.""" + rule_ids: list[str] = [] + for match in RULE_ANNOTATION_RE.finditer(fixture_text): + for raw_rule_id in match.group(1).split(","): + rule_id = raw_rule_id.strip() + if rule_id.startswith("cdt.") and rule_id not in rule_ids: + rule_ids.append(rule_id) + return rule_ids + + +def config_rule_chunks(config_text: str) -> dict[str, str]: + """Return YAML chunks keyed by Semgrep rule ID.""" + chunks: dict[str, str] = {} + for chunk in RULE_SPLIT_RE.split(config_text)[1:]: + lines = chunk.splitlines() + if not lines: + continue + rule_id = lines[0].strip() + chunks[rule_id] = f" - id: {chunk}" + return chunks + + +def build_fixture_config(fixture_path: Path, source_config_path: Path) -> str: + """Build the minimal Semgrep config needed to test one fixture.""" + annotation_ids = annotated_rule_ids(fixture_path.read_text(encoding=UTF8)) + rule_chunks = config_rule_chunks(source_config_path.read_text(encoding=UTF8)) + missing_rule_ids = [rule_id for rule_id in annotation_ids if rule_id not in rule_chunks] + if missing_rule_ids: + missing_rules = ", ".join(missing_rule_ids) + msg = f"missing Semgrep rules for fixture {fixture_path}: {missing_rules}" + raise ValueError(msg) + return "rules:\n" + "".join(rule_chunks[rule_id] for rule_id in annotation_ids) + + +def parse_args(argv: "Sequence[str]") -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("fixture", type=Path) + parser.add_argument("source_config", type=Path) + parser.add_argument("output_config", type=Path) + return parser.parse_args(argv) + + +def main(argv: "Sequence[str] | None" = None) -> int: + """Write the selected rules beside the mirrored fixture path.""" + args = parse_args(sys.argv[1:] if argv is None else argv) + try: + config = build_fixture_config(args.fixture, args.source_config) + args.output_config.write_text(config, encoding=UTF8) + except ValueError as exc: + print(exc, file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/tests/__init__.py b/scripts/tests/__init__.py new file mode 100644 index 0000000000..c96d6b1916 --- /dev/null +++ b/scripts/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for repository-owned support scripts.""" diff --git a/scripts/tests/test_optimize_initialize.py b/scripts/tests/test_optimize_initialize.py new file mode 100644 index 0000000000..582cc272a6 --- /dev/null +++ b/scripts/tests/test_optimize_initialize.py @@ -0,0 +1,38 @@ +"""Tests for the portable initializer optimization support script.""" + +from __future__ import annotations + +import unittest +from pathlib import Path + +from scripts.optimize_initialize import _initializer_binary, _parse_initializer_output + + +class OptimizeInitializeTests(unittest.TestCase): + """Verify dependency-free parsing and executable discovery.""" + + def test_initializer_binary_uses_windows_suffix(self) -> None: + """Windows reference builds end in initialize.exe.""" + root = Path("checkout") + self.assertEqual( + _initializer_binary(root, "win32"), + root / "out" / "build" / "reference" / "src" / "initialize.exe", + ) + + def test_initializer_binary_has_no_unix_suffix(self) -> None: + """Unix reference builds retain the extensionless executable name.""" + root = Path("checkout") + expected = root / "out" / "build" / "reference" / "src" / "initialize" + self.assertEqual(_initializer_binary(root, "darwin"), expected) + self.assertEqual(_initializer_binary(root, "linux"), expected) + + def test_initializer_output_is_parsed(self) -> None: + """The sweep extracts both the final size and volume profile.""" + output = """Timeslice 1 has 12 spacelike faces. +Timeslice 2 has 24 spacelike faces. +Final number of simplices: 92""" + self.assertEqual(_parse_initializer_output(output), (92, [(1, 12), (2, 24)])) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/tests/test_release_check.py b/scripts/tests/test_release_check.py new file mode 100644 index 0000000000..4c859e390f --- /dev/null +++ b/scripts/tests/test_release_check.py @@ -0,0 +1,91 @@ +"""Tests for release metadata validation.""" + +from __future__ import annotations + +import tempfile +import unittest +from pathlib import Path + +from scripts import release_check + + +def _write_project(root: Path, *, metadata_version: str = "1.2.3-rc4", release_date: str = "2026-07-20") -> None: + """Write a minimal synchronized release-metadata fixture.""" + pep440_version = metadata_version.replace("-rc", "rc") + files = { + "CMakeLists.txt": ('project(\n CDT-plusplus\n VERSION 1.2.3\n DESCRIPTION "fixture"\n LANGUAGES CXX)\nset(CDT_VERSION_SUFFIX "-rc4")\n'), + "vcpkg.json": f'{{"name": "cdt-plusplus", "version": "{metadata_version}"}}\n', + "pyproject.toml": f'[project]\nname = "cdt-plusplus-scripts"\nversion = "{pep440_version}"\n', + "uv.lock": (f'version = 1\n\n[[package]]\nname = "cdt-plusplus-scripts"\nversion = "{pep440_version}"\nsource = {{ editable = "." }}\n'), + "CITATION.cff": ( + "cff-version: 1.2.0\n" + 'message: "Cite this software."\n' + "type: software\n" + 'title: "CDT++"\n' + 'abstract: "A fixture."\n' + "authors:\n" + ' - family-names: "Getchell"\n' + ' given-names: "Adam"\n' + f'version: "{metadata_version}"\n' + f'date-released: "{release_date}"\n' + 'repository-code: "https://example.com/repository"\n' + 'url: "https://example.com"\n' + 'license: "BSD-3-Clause"\n' + ), + "docs/Doxyfile": f"PROJECT_NUMBER = {metadata_version}\n", + "README.md": f"Current release: v{metadata_version}.\n", + "REFERENCES.md": f"Version {metadata_version}.\n", + ".github/CONTRIBUTING.md": f"Contributing to v{metadata_version}.\n", + } + for relative_path, content in files.items(): + path = root / relative_path + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +class ReleaseCheckTests(unittest.TestCase): + """Exercise synchronized and malformed release fixtures.""" + + def test_accepts_synchronized_release_metadata(self) -> None: + """Matching metadata returns the product version and release date.""" + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + _write_project(root) + + version, release_date = release_check.check_release_metadata(root) + + self.assertEqual(version, "1.2.3-rc4") + self.assertEqual(release_date.isoformat(), "2026-07-20") + + def test_rejects_structured_version_drift(self) -> None: + """A stale package version fails with its owning file.""" + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + _write_project(root) + (root / "vcpkg.json").write_text('{"name": "cdt-plusplus", "version": "1.2.3-rc3"}\n', encoding="utf-8") + + with self.assertRaisesRegex(release_check.ReleaseCheckError, "vcpkg.json version=1.2.3-rc3"): + release_check.check_release_metadata(root) + + def test_rejects_invalid_citation_date(self) -> None: + """CFF release dates must be real ISO calendar dates.""" + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + _write_project(root, release_date="2026-02-30") + + with self.assertRaisesRegex(release_check.ReleaseCheckError, "ISO calendar date"): + release_check.check_release_metadata(root) + + def test_rejects_stale_active_documentation(self) -> None: + """Active documentation may not retain an older RC version.""" + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + _write_project(root) + (root / "README.md").write_text("Current release: v1.2.3-rc3.\n", encoding="utf-8") + + with self.assertRaisesRegex(release_check.ReleaseCheckError, "stale release-candidate versions"): + release_check.check_release_metadata(root) + + +if __name__ == "__main__": + unittest.main() diff --git a/semgrep.yaml b/semgrep.yaml new file mode 100644 index 0000000000..f2c74ba39b --- /dev/null +++ b/semgrep.yaml @@ -0,0 +1,203 @@ +--- +# Repository-owned Semgrep rules for narrow CDT++ maintenance invariants. +rules: + - id: cdt.cpp.no-routine-output-in-move-hot-paths + languages: + - cpp + severity: WARNING + message: >- + Keep proposal, application, and move-tracking hot paths silent. Report + pass-level summaries outside these paths, and reserve warn/error logs for + genuine invariant failures. + metadata: + category: maintainability + tracking_issue: "https://github.com/acgetchell/CDT-plusplus/issues/92" + rationale: >- + Per-proposal diagnostics obscure useful simulation progress and impose + avoidable formatting and logging overhead in the hottest move paths. + paths: + include: + - "/include/Apply_move.hpp" + - "/include/Ergodic_moves_3.hpp" + - "/include/Move_tracker.hpp" + - "/tests/semgrep/**/*.cpp" + pattern-either: + - pattern: spdlog::trace(...) + - pattern: spdlog::debug(...) + - pattern: spdlog::info(...) + - pattern: fmt::print(...) + - id: cdt.cpp.random-entropy-is-centralized + languages: + - cpp + severity: ERROR + message: >- + Acquire operating-system entropy only in Random.hpp. Construct one + cdt::Random per run and pass it by reference through stochastic code. + metadata: + category: correctness + tracking_issue: "https://github.com/acgetchell/CDT-plusplus/issues/105" + rationale: >- + Per-draw entropy prevents replay, hides stochastic state, and is much + slower than sampling a run-owned PCG stream. + paths: + include: + - "/include/**/*.hpp" + - "/src/**/*.cpp" + - "/tests/*_test.cpp" + - "/tests/**/*_test.cpp" + - "/tests/semgrep/**/*.cpp" + exclude: + - "/include/Random.hpp" + pattern-either: + - pattern: std::random_device $ENTROPY; + - pattern: std::random_device $ENTROPY(...); + - pattern-regex: >- + \bstd::random_device[ \t]+[A-Za-z_][A-Za-z0-9_]*[ \t]*\{[ \t]*\}[ \t]*; + - id: cdt.cpp.use-repository-random-abstraction + languages: + - cpp + severity: ERROR + message: >- + Use cdt::Random instead of constructing a PCG engine directly so seeds + and stream identifiers remain observable and replayable. + metadata: + category: correctness + tracking_issue: "https://github.com/acgetchell/CDT-plusplus/issues/105" + rationale: >- + Centralizing PCG ownership separates engines from distributions and + gives simulation, test, and future worker streams one explicit policy. + paths: + include: + - "/include/**/*.hpp" + - "/src/**/*.cpp" + - "/tests/*_test.cpp" + - "/tests/**/*_test.cpp" + - "/tests/semgrep/**/*.cpp" + exclude: + - "/include/Random.hpp" + pattern-either: + - pattern: pcg32 $ENGINE; + - pattern: pcg32 $ENGINE(...); + - pattern: pcg64 $ENGINE; + - pattern: pcg64 $ENGINE(...); + - pattern-regex: >- + \bpcg(?:32|64)[ \t]+[A-Za-z_][A-Za-z0-9_]*[ \t]*\{[^{};]*\}[ \t]*; + - id: cdt.cpp.no-hidden-random-construction + languages: + - cpp + severity: ERROR + message: >- + Stochastic helpers must receive cdt::Random from their caller. Default + construction belongs only at an observable run boundary. + metadata: + category: correctness + tracking_issue: "https://github.com/acgetchell/CDT-plusplus/issues/105" + rationale: >- + Wrapping per-call entropy in cdt::Random would still hide ownership and + defeat replay even though direct std::random_device use is centralized. + paths: + include: + - "/include/**/*.hpp" + - "/src/**/*.cpp" + - "/tests/*_test.cpp" + - "/tests/**/*_test.cpp" + - "/tests/semgrep/**/*.cpp" + exclude: + - "/include/Random.hpp" + - "/include/Metropolis.hpp" + - "/include/Move_always.hpp" + - "/src/cdt.cpp" + - "/src/initialize.cpp" + pattern-either: + - pattern: cdt::Random $RANDOM; + - pattern: cdt::Random $RANDOM(); + - pattern: cdt::Random() + - pattern-regex: >- + \bcdt::Random(?:[ \t]+[A-Za-z_][A-Za-z0-9_]*)?[ \t]*\{[ \t]*\}[ \t]*;? + - id: cdt.cpp.no-skipped-doctests + languages: + - cpp + severity: ERROR + message: >- + Do not commit skipped doctest scenarios. Implement the missing fixture or + remove the scenario so the reported suite is entirely executable. + metadata: + category: correctness + rationale: >- + A skipped scenario inflates the apparent test inventory without + supplying executable regression evidence. + paths: + include: + - "/tests/*_test.cpp" + - "/tests/**/*_test.cpp" + - "/tests/semgrep/**/*.cpp" + pattern: doctest::skip(...) + - id: cdt.cpp.doctest-framework-is-test-only + languages: + - cpp + severity: ERROR + message: >- + Keep doctest headers, configuration, and scenario definitions under + tests/. Production targets must not own or execute the test harness. + metadata: + category: architecture + rationale: >- + Test-framework ownership in production code couples shipped binaries + to the test harness and can silently fragment test registration. + paths: + include: + - "/include/**/*.hpp" + - "/src/**/*.cpp" + - "/tests/semgrep/**/*.cpp" + pattern-either: + - pattern-regex: |- + (?m)^[ \t]*#[ \t]*include[ \t]*[<"]doctest(?:/[^>"]*)?[>"] + - pattern-regex: |- + (?m)^[ \t]*#[ \t]*define[ \t]+DOCTEST_CONFIG_[A-Za-z0-9_]+ + - pattern-regex: |- + (?m)^[ \t]*(?:SCENARIO|TEST_CASE)(?:_TEMPLATE(?:_DEFINE|_INVOKE)?)?[ \t]*\( + - id: cdt.cpp.no-diagnostic-output-in-doctests + languages: + - cpp + severity: WARNING + message: >- + Replace diagnostic-only test output with assertions on observable state. + Test an intentional printing contract through an isolated captured stream. + metadata: + category: test-quality + rationale: >- + Unasserted output adds noise while allowing the test to pass regardless + of what was printed. + paths: + include: + - "/tests/*_test.cpp" + - "/tests/**/*_test.cpp" + - "/tests/semgrep/**/*.cpp" + pattern-either: + - pattern: fmt::print(...) + - pattern: std::cout << $VALUE; + - patterns: + - pattern: $OBJECT.$METHOD(...) + - metavariable-regex: + metavariable: $METHOD + regex: ^print(?:_|$) + - id: cdt.cpp.test-temp-paths-are-unique + languages: + - cpp + severity: ERROR + message: >- + Do not place a fixed filename directly under the shared temporary + directory. Use a unique RAII-owned directory and clean it up with the + fixture. + metadata: + category: correctness + rationale: >- + Fixed process-global temporary paths collide across parallel runs and + can preserve state between otherwise independent tests. + paths: + include: + - "/tests/*_test.cpp" + - "/tests/**/*_test.cpp" + - "/tests/semgrep/**/*.cpp" + pattern-regex: |- + (?:std::filesystem|filesystem|fs)::temp_directory_path\(\)[ \t]*/[ \t]*(?:u8|u|U|L)?"[^"\r\n]*" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c3de3d1000..a9b5ae45bb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -35,11 +35,14 @@ function(add_cli_failure_test test_name target expected_regex) ${CMAKE_COMMAND} "-DTEST_EXECUTABLE=$" "-DTEST_ARGUMENTS=${ARGN}" "-DEXPECTED_REGEX=${expected_regex}" -P ${PROJECT_SOURCE_DIR}/cmake/ExpectCommandFailure.cmake) - set_tests_properties(${test_name} PROPERTIES LABELS "cli-boundary") + set_tests_properties(${test_name} PROPERTIES LABELS "integration;cli-boundary") endfunction() -add_test(NAME cdt COMMAND $ -s -n127 -t4 -a0.6 -k1.1 -l0.1 -p10) -set_tests_properties(cdt PROPERTIES PASS_REGULAR_EXPRESSION "Writing to file S3-[0-9]+-") +add_test(NAME cdt COMMAND $ -s -n127 -t4 -a0.6 -k1.1 -l0.1 -p1 --seed 92) +set_tests_properties( + cdt + PROPERTIES LABELS "integration" + PASS_REGULAR_EXPRESSION "Writing to file S3-[0-9]+-") add_test( NAME cdt-no-output @@ -47,38 +50,38 @@ add_test( ${CMAKE_COMMAND} -DCDT_EXECUTABLE=$ -DTEST_DIRECTORY=${CMAKE_CURRENT_BINARY_DIR}/cdt-no-output -P ${PROJECT_SOURCE_DIR}/cmake/RunCdtNoOutputTest.cmake) +set_tests_properties(cdt-no-output PROPERTIES LABELS "integration") add_cli_failure_test(cdt-triangle-inequalities cdt "Triangle inequalities violated" -s -n64 -t3 -a0.4 - -k1.1 -l0.1) + -k1.1 -l0.1 --seed 92) add_cli_failure_test(cdt-dimensionality cdt "Only three-dimensional triangulations are supported." -s -n64 -t3 - -d4 -a0.4 -k1.1 -l0.1) + -d4 -a0.4 -k1.1 -l0.1 --seed 92) add_cli_failure_test(cdt-toroidal cdt "Toroidal triangulations are not yet supported." -e -n64 -t3 -a0.6 -k1.1 - -l0.1) + -l0.1 --seed 92) add_cli_failure_test(cdt-alpha-negative cdt "Alpha in 3D must be greater than 1/2." -s -n64 -t3 -a-0.6 -k1.1 - -l0.1) + -l0.1 --seed 92) add_cli_failure_test(cdt-alpha-boundary cdt "Alpha in 3D must be greater than 1/2." -s -n64 -t3 -a0.5 -k1.1 - -l0.1) -add_cli_failure_test(cdt-alpha-nonfinite cdt "Alpha must be finite." -s -n64 -t3 -anan -k1.1 -l0.1) -add_cli_failure_test(cdt-passes-zero cdt "Passes must be positive." -s -n64 -t3 -a0.6 -k1.1 -l0.1 -p0) + -l0.1 --seed 92) +add_cli_failure_test(cdt-alpha-nonfinite cdt "Alpha must be finite." -s -n64 -t3 -anan -k1.1 -l0.1 --seed 92) +add_cli_failure_test(cdt-passes-zero cdt "Passes must be positive." -s -n64 -t3 -a0.6 -k1.1 -l0.1 -p0 --seed 92) add_cli_failure_test(cdt-checkpoint-zero cdt "Checkpoint interval must be positive." -s -n64 -t3 -a0.6 -k1.1 - -l0.1 -c0) -add_cli_failure_test(cdt-radius-zero cdt "Initial radius must be positive." -s -n64 -t3 -a0.6 -k1.1 -l0.1 -i0) -add_cli_failure_test(cdt-spacing-zero cdt "Foliation spacing must be positive." -s -n64 -t3 -a0.6 -k1.1 -l0.1 -f0) -add_cli_failure_test(cdt-empty-population cdt "would create an empty triangulation" -s -n3 -t2 -a0.6 -k1.1 -l0.1) + -l0.1 -c0 --seed 92) +add_cli_failure_test(cdt-radius-zero cdt "Initial radius must be positive." -s -n64 -t3 -a0.6 -k1.1 -l0.1 -i0 --seed 92) +add_cli_failure_test(cdt-spacing-zero cdt "Foliation spacing must be positive." -s -n64 -t3 -a0.6 -k1.1 -l0.1 -f0 --seed 92) +add_cli_failure_test(cdt-empty-population cdt "would create an empty triangulation" -s -n3 -t2 -a0.6 -k1.1 -l0.1 --seed 92) add_cli_failure_test(cdt-integer-narrowing cdt "exceeds the supported integer range" -s -n2147483648 -t3 -a0.6 - -k1.1 -l0.1) + -k1.1 -l0.1 --seed 92) -add_test(NAME initialize COMMAND $ -s -n640 -t4 -o) -set_tests_properties(initialize PROPERTIES PASS_REGULAR_EXPRESSION "Writing to file S3-4") -if(WIN32) - # Preserve the known failing scenario for explicit debugging without blocking the portable smoke suite. - set_tests_properties(initialize PROPERTIES LABELS "reference-smoke-exclude") -endif() +add_test(NAME initialize COMMAND $ -s -n640 -t4 -o --seed 92) +set_tests_properties( + initialize + PROPERTIES LABELS "integration" + PASS_REGULAR_EXPRESSION "Writing to file S3-4") add_cli_failure_test(initialize-minimum-simplices initialize "Simplices and timeslices must each be at least 2." -s -n1 - -t1 -o) + -t1 -o --seed 92) add_cli_failure_test(initialize-dimensionality initialize "Only three-dimensional triangulations are supported." -s - -n64 -t3 -d2 -o) -add_cli_failure_test(initialize-toroidal initialize "Toroidal triangulations are not yet supported." -e -n64 -t3 -o) -add_cli_failure_test(initialize-integer-narrowing initialize "exceeds the supported integer range" -s -n2147483648 -t3) -add_cli_failure_test(initialize-spacing-zero initialize "Foliation spacing must be positive." -s -n64 -t3 -f0) + -n64 -t3 -d2 -o --seed 92) +add_cli_failure_test(initialize-toroidal initialize "Toroidal triangulations are not yet supported." -e -n64 -t3 -o --seed 92) +add_cli_failure_test(initialize-integer-narrowing initialize "exceeds the supported integer range" -s -n2147483648 -t3 --seed 92) +add_cli_failure_test(initialize-spacing-zero initialize "Foliation spacing must be positive." -s -n64 -t3 -f0 --seed 92) diff --git a/src/cdt-viewer.cpp b/src/cdt-viewer.cpp index f2a4984902..6841a8fbff 100644 --- a/src/cdt-viewer.cpp +++ b/src/cdt-viewer.cpp @@ -7,21 +7,14 @@ Copyright © 2022 Adam Getchell /// @brief Views 3D spacetimes /// @author Adam Getchell -#ifdef NDEBUG -#define DOCTEST_CONFIG_DISABLE -#endif - #include - -#define DOCTEST_CONFIG_IMPLEMENT - -#include #include #include #include "Manifold.hpp" #include "Utilities.hpp" +#include "Version.hpp" namespace po = boost::program_options; @@ -41,22 +34,6 @@ Options)"; auto main(int const argc, char* const argv[]) -> int try { - // Doctest integration into code - doctest::Context context; - context.setOption("no-breaks", - true); // don't break in debugger when assertions fail - context.applyCommandLine(argc, argv); - - int const res = context.run(); // run tests unless --no-run is specified - if (context.shouldExit()) - { // important - query flags (and --exit) rely on the user doing this - return res; // propagate the result of the tests - } - - context.clearFilters(); // important - otherwise the context filters will be - // used during the next evaluation of RUN_ALL_TESTS, - // which will lead to wrong results - std::string const intro{USAGE}; // Parsed arguments std::string filename; @@ -75,19 +52,19 @@ try if (args.count("help")) { std::cout << description << "\n"; - return res + EXIT_SUCCESS; + return EXIT_SUCCESS; } if (args.count("version")) { - fmt::print("cdt-viewer 1.0\n"); - return res + EXIT_SUCCESS; + fmt::print("cdt-viewer version {}\n", cdt::VERSION); + return EXIT_SUCCESS; } if (args.count("dry-run")) { fmt::print("Dry run. Exiting.\n"); - return res + EXIT_SUCCESS; + return EXIT_SUCCESS; } fmt::print("cdt-viewer started at {}\n", utilities::current_date_time()); @@ -101,7 +78,7 @@ try fmt::print("Drawing {}\n", filename); draw(dt_in); - return res + EXIT_SUCCESS; + return EXIT_SUCCESS; } catch (std::exception const& e) @@ -115,49 +92,3 @@ catch (...) spdlog::critical("Something went wrong ... Exiting.\n"); return EXIT_FAILURE; } - -SCENARIO("Given a 3D Manifold, it can be written to file and read back in." * - doctest::test_suite("cdt-viewer")) -{ - GIVEN("A 3D Manifold.") - { - auto constexpr simplices = 640; - auto constexpr timeslices = 4; - manifolds::Manifold_3 const manifold(simplices, timeslices); - - WHEN("It is written to file.") - { - auto const filename = utilities::make_filename(manifold); - utilities::write_file(manifold); - - THEN("It can be read back in.") - { - auto dt_in = utilities::read_file>(filename); - REQUIRE(dt_in.is_valid(true)); - REQUIRE_EQ(dt_in.dimension(), manifold.dimensionality()); - REQUIRE_EQ(dt_in.number_of_finite_cells(), manifold.N3()); - REQUIRE_EQ(dt_in.number_of_finite_facets(), manifold.N2()); - REQUIRE_EQ(dt_in.number_of_finite_edges(), manifold.N1()); - REQUIRE_EQ(dt_in.number_of_vertices(), manifold.N0()); - } - THEN("It can be drawn.") - { - auto const dt_in = utilities::read_file>(filename); - CGAL::draw(dt_in); - // Cleanup test file - REQUIRE_NOTHROW(std::filesystem::remove(filename)); - } - } - } - GIVEN("A non-existent filename.") - { - WHEN("It is read back in.") - { - THEN("An exception is thrown.") - { - REQUIRE_THROWS_AS(utilities::read_file>("unused.off"), - std::filesystem::filesystem_error); - } - } - } -} diff --git a/src/cdt.cpp b/src/cdt.cpp index 3ac1b7df67..bb53db1e4e 100644 --- a/src/cdt.cpp +++ b/src/cdt.cpp @@ -13,10 +13,12 @@ #include #include +#include #include #include #include "Runtime_config.hpp" +#include "Version.hpp" using Timer = CGAL::Real_timer; @@ -40,6 +42,7 @@ Usage:./cdt (--spherical | --toroidal) -n SIMPLICES -t TIMESLICES [--init INITIAL RADIUS] [--foliate FOLIATION SPACING] [--no-output] + [--seed SEED] -k K --alpha ALPHA --lambda LAMBDA @@ -50,7 +53,7 @@ Optional arguments are in square brackets. Examples: ./cdt --spherical -n 32000 -t 11 --alpha 0.6 -k 1.1 --lambda 0.1 --passes 1000 -./cdt -s -n32000 -t11 -a.6 -k1.1 -l.1 -p1000 +./cdt -s -n32000 -t11 -a.6 -k1.1 -l.1 -p1000 --seed 92 Options)"}; @@ -73,6 +76,7 @@ try long double lambda{}; long long passes{}; long long checkpoint{}; + std::uint64_t seed{}; po::options_description description(intro); description.add_options()("help,h", "Show this message")( @@ -89,6 +93,8 @@ try "foliate,f", po::value(&foliation_spacing)->default_value(1.0), "Foliation spacing")( "no-output", "Do not write checkpoint or final triangulation files")( + "seed", po::value(&seed), + "Root random seed (default: operating-system entropy)")( "alpha,a", po::value(&alpha)->required(), "Negative squared geodesic length of 1-d timelike edges")( "k,k", po::value(&k)->required(), "K = 1/(8*pi*G_newton)")( @@ -110,7 +116,7 @@ try if (args.count("version")) { - fmt::print("CDT++ version 0.1.8\n"); + fmt::print("CDT++ version {}\n", cdt::VERSION); return EXIT_SUCCESS; } @@ -130,6 +136,11 @@ try auto const config = runtime_config::make_simulation( triangulation_config, alpha, k, lambda, passes, checkpoint, !args.count("no-output")); + auto root_random = + args.count("seed") != 0 ? cdt::Random{seed} : cdt::Random{}; + auto initialization_random = + root_random.split(cdt::random_streams::initialization); + auto transition_random = root_random.split(cdt::random_streams::transitions); // Display job parameters fmt::print("Topology is {}\n", @@ -145,6 +156,7 @@ try config.triangulation().timeslices()); fmt::print("Number of passes: {}\n", config.passes()); fmt::print("Checkpoint every {} passes.\n", config.checkpoint()); + fmt::print("Effective random seed: {}\n", root_random.seed()); fmt::print("=== Parameters ===\n"); fmt::print("Alpha: {}\n", config.alpha()); fmt::print("K: {}\n", config.k()); @@ -157,14 +169,15 @@ try // Initialize the Metropolis algorithm Metropolis_3 run(config.alpha(), config.k(), config.lambda(), config.passes(), - config.checkpoint(), config.write_files()); + config.checkpoint(), config.write_files(), + std::move(transition_random)); // Make a triangulation manifolds::Manifold_3 universe; manifolds::Manifold_3 populated_universe( config.triangulation().simplices(), config.triangulation().timeslices(), - config.triangulation().initial_radius(), + initialization_random, config.triangulation().initial_radius(), config.triangulation().foliation_spacing()); swap(populated_universe, universe); @@ -195,7 +208,10 @@ try result.print_volume_per_timeslice(); // Write results to file - if (config.write_files()) { utilities::write_file(result); } + if (config.write_files()) + { + utilities::write_file(result, root_random.seed(), config.passes()); + } return EXIT_SUCCESS; } diff --git a/src/initialize.cpp b/src/initialize.cpp index 3a6c4181b6..03c5a5cff4 100644 --- a/src/initialize.cpp +++ b/src/initialize.cpp @@ -9,9 +9,11 @@ /// @author Adam Getchell #include +#include #include "Manifold.hpp" #include "Runtime_config.hpp" +#include "Version.hpp" using namespace std; namespace po = boost::program_options; @@ -30,13 +32,14 @@ Usage:./initialize (--spherical | --toroidal) -n SIMPLICES -t TIMESLICES [-d DIM] [--init INITIAL RADIUS] [--foliate FOLIATION SPACING] + [--seed SEED] [--output] Optional arguments are in square brackets. Examples: ./initialize --spherical --simplices 32000 --timeslices 11 --init 1.0 --foliate 1.0 --output -./initialize -s -n32000 -t11 -i1.0 -f1.0 -o +./initialize -s -n32000 -t11 -i1.0 -f1.0 -o --seed 92 Options)"}; @@ -50,6 +53,7 @@ try long long dimensions{}; double initial_radius{}; double foliation_spacing{}; + std::uint64_t seed{}; po::options_description description(intro); description.add_options()("help,h", "Show this message")( @@ -64,7 +68,10 @@ try po::value(&initial_radius)->default_value(1.0), "Initial radius")( "foliate,f", po::value(&foliation_spacing)->default_value(1.0), - "Foliation spacing")("output,o", "Save triangulation into OFF file"); + "Foliation spacing")( + "seed", po::value(&seed), + "Root random seed (default: operating-system entropy)")( + "output,o", "Save triangulation into OFF file"); po::variables_map args; po::store(po::parse_command_line(argc, argv, description), args); @@ -77,7 +84,7 @@ try if (args.count("version")) { - fmt::print("CDT initializer version 1.0\n"); + fmt::print("CDT initializer version {}\n", cdt::VERSION); return EXIT_SUCCESS; } @@ -96,6 +103,10 @@ try args.count("spherical") != 0, args.count("toroidal") != 0, simplices, timeslices, dimensions, initial_radius, foliation_spacing); auto const save_file = args.count("output") != 0; + auto root_random = + args.count("seed") != 0 ? cdt::Random{seed} : cdt::Random{}; + auto initialization_random = + root_random.split(cdt::random_streams::initialization); // Display job parameters fmt::print("Topology is {}\n", utilities::topology_to_str(config.topology())); @@ -104,16 +115,17 @@ try fmt::print("Number of desired timeslices = {}\n", config.timeslices()); fmt::print("Initial radius = {}\n", config.initial_radius()); fmt::print("Foliation spacing = {}\n", config.foliation_spacing()); + fmt::print("Effective random seed: {}\n", root_random.seed()); if (save_file) { fmt::print("Output will be saved.\n"); } manifolds::Manifold_3 universe(config.simplices(), config.timeslices(), - config.initial_radius(), + initialization_random, config.initial_radius(), config.foliation_spacing()); universe.print(); universe.print_volume_per_timeslice(); fmt::print("Final number of simplices: {}\n", universe.N3()); - if (save_file) { utilities::write_file(universe); } + if (save_file) { utilities::write_file(universe, root_random.seed()); } return EXIT_SUCCESS; } catch (invalid_argument const& InvalidArgument) diff --git a/src/optimize-initialize.py b/src/optimize-initialize.py deleted file mode 100644 index 22b9e0a4aa..0000000000 --- a/src/optimize-initialize.py +++ /dev/null @@ -1,147 +0,0 @@ -# Causal Dynamical Triangulations in C++ using CGAL -# -# Copyright © 2018 Adam Getchell -# -# A program that optimizes spacetime generation parameters - -# @file optimize-initialize.py -# @brief Optimize spacetime generation -# @author Adam Getchell - -# Usage: python optimize-initialize.py -# - -"""Run the legacy CDT++ parameter optimization experiment.""" - -import os -import re -from pathlib import Path -from subprocess import check_output as qx - -# from comet_ml import Optimizer -import comet_ml as cm - -# Import TensorFlow -# import tensorflow as tf -# import tensorflow.contrib.eager as tfe -import matplotlib.pyplot as plt -import numpy as np - -# Import Comet.ml -from comet_ml import Experiment - -# Create an optimizer for dynamic parameters -# optimizer = Optimizer(api_key=os.environ['COMET_API_KEY']) -# params = """ -# initial_radius integer [1, 2] [1] -# foliation_spacing integer [1, 2] [1] -# """ -# -# optimizer.set_params(params) - -# tf.enable_eager_execution() - -# Create parameters to vary -parameters = [(initial_radius, spacing) for initial_radius in range(1, 4) for spacing in np.arange(1, 2.5, 0.5)] - -repository_root = Path(__file__).resolve().parents[1] -initialize_binary = repository_root / "out/build/reference/src/initialize" -if not initialize_binary.is_file(): - missing_binary_message = f"CDT++ initializer not found at {initialize_binary}; run `just build` first." - raise SystemExit(missing_binary_message) - -api_key = os.environ.get("COMET_API_KEY") -if not api_key: - missing_api_key_message = "COMET_API_KEY is required for an online Comet experiment." - raise SystemExit(missing_api_key_message) - -try: - # while True: - # Get a suggestion - # suggestion = optimizer.get_suggestion() - for parameter_pair in parameters: - # Create an experiment with api key - experiment = Experiment(api_key=api_key, project_name="cdt-plusplus") - - # print('TensorFlow version: {}'.format(tf.VERSION)) - - hyper_params = {"simplices": 12000, "foliations": 12} - experiment.log_multiple_params(hyper_params) - # init_radius = suggestion["initial_radius"] - init_radius = parameter_pair[0] - # radial_factor = suggestion["foliation_spacing"] - radial_factor = parameter_pair[1] - - args = [ - str(initialize_binary), - "-s", - "-n", - str(hyper_params["simplices"]), - "-t", - str(hyper_params["foliations"]), - "-i", - str(init_radius), - "-f", - str(radial_factor), - ] - - print(args) - - # The argument vector is assembled entirely from repository-controlled - # executable and numeric experiment parameters. - output = qx(args, text=True) # noqa: S603 - - final_simplices = None - graph: list[tuple[int, int]] = [] - for line in output.splitlines(): - if match := re.fullmatch(r"Final number of simplices: (?P\d+)", line): - final_simplices = int(match.group("count")) - elif line.startswith("Timeslice"): - match = re.fullmatch(r"Timeslice (?P\d+) has (?P\d+) spacelike faces\.", line) - if match: - graph.append((int(match.group("timeslice")), int(match.group("volume")))) - - if final_simplices is None: - missing_simplices_message = "Initializer output did not report the final number of simplices." - raise RuntimeError(missing_simplices_message) - if not graph: - missing_profile_message = "Initializer output did not contain a timeslice volume profile." - raise RuntimeError(missing_profile_message) - - min_timeslice = min(timeslice for timeslice, _ in graph) - max_timeslice = max(timeslice for timeslice, _ in graph) - result = (final_simplices, min_timeslice, max_timeslice) - - print(result) - print(f"Initial radius is: {init_radius}") - print(f"Radial factor is: {radial_factor}") - for timeslice, volume in graph: - print(f"Timeslice {timeslice} has {volume} spacelike faces.") - print() - - # Score model - target_simplices = hyper_params["simplices"] - score = ((final_simplices - target_simplices) / target_simplices) * 100 - - # Report results - experiment.log_metric("Error %", score) - experiment.log_other("Min Timeslice", result[1]) - experiment.log_other("Max Timeslice", result[2]) - - # Graph volume profile - timeslices = [timeslice for timeslice, _ in graph] - volumes = [volume for _, volume in graph] - plt.plot(timeslices, volumes) - plt.xlabel("Timeslice") - plt.ylabel("Volume (spacelike faces)") - plt.title("Volume Profile") - plt.grid(visible=True) - experiment.log_figure(figure_name="Volume per Timeslice", figure=plt) - plt.clf() - experiment.end() - - -except cm.exceptions.NoMoreSuggestionsAvailable: - print("No more suggestions.") - -print("All done with parameter optimization, look at Comet.ml for results.") diff --git a/src/test.py b/src/test.py deleted file mode 100644 index 8e0fe5852c..0000000000 --- a/src/test.py +++ /dev/null @@ -1,45 +0,0 @@ -# Causal Dynamical Triangulations in C++ using CGAL -# -# Copyright © 2021 Adam Getchell -# -# First pass at ML for CDT++ - -# @file test.py -# @brief ML using TensorFlow -# @author Adam Getchell - -# Usage: python test.py -# - -"""Run the legacy TensorFlow MNIST experiment.""" - -import tensorflow as tf - -tf.keras.utils.set_random_seed(0) - -mnist = tf.keras.datasets.mnist - -(x_train, y_train), (x_test, y_test) = mnist.load_data() -x_train, x_test = x_train / 255.0, x_test / 255.0 - -model = tf.keras.models.Sequential( - [tf.keras.layers.Flatten(input_shape=(28, 28)), tf.keras.layers.Dense(128, activation="relu"), tf.keras.layers.Dropout(0.2), tf.keras.layers.Dense(10)] -) - -predictions = model(x_train[:1]).numpy() - -tf.nn.softmax(predictions).numpy() - -loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True) - -loss_fn(y_train[:1], predictions).numpy() - -model.compile(optimizer="adam", loss=loss_fn, metrics=["accuracy"]) - -model.fit(x_train, y_train, epochs=5) - -model.evaluate(x_test, y_test, verbose=2) - -probability_model = tf.keras.Sequential([model, tf.keras.layers.Softmax()]) - -probability_model(x_test[:5]) diff --git a/tests/Apply_move_test.cpp b/tests/Apply_move_test.cpp index aeaf09c43c..922fb006ca 100644 --- a/tests/Apply_move_test.cpp +++ b/tests/Apply_move_test.cpp @@ -13,175 +13,171 @@ #include +#include +#include +#include + #include "Ergodic_moves_3.hpp" using namespace std; +using namespace manifolds; + +namespace +{ + static inline auto constexpr RADIUS_2 = + 2.0 * std::numbers::inv_sqrt3_v; + static inline auto constexpr SQRT_2 = std::numbers::sqrt2_v; + static inline auto constexpr INV_SQRT_2 = 1.0 / SQRT_2; + + [[nodiscard]] auto make_23_move_manifold() -> Manifold_3 + { + vector vertices{ + Point_t<3>{ 1, 0, 0}, + Point_t<3>{ 0, 1, 0}, + Point_t<3>{ 0, 0, 1}, + Point_t<3>{RADIUS_2, RADIUS_2, RADIUS_2}, + Point_t<3>{ SQRT_2, SQRT_2, 0} + }; + vector const timevalues{1, 1, 1, 2, 2}; + return Manifold_3{make_causal_vertices<3>(vertices, timevalues)}; + } + + [[nodiscard]] auto make_26_move_manifold() -> Manifold_3 + { + vector vertices{ + Point_t<3>{ 0, 0, 0}, + Point_t<3>{ 1, 0, 0}, + Point_t<3>{ 0, 1, 0}, + Point_t<3>{ 0, 0, 1}, + Point_t<3>{RADIUS_2, RADIUS_2, RADIUS_2} + }; + vector const timevalues{0, 1, 1, 1, 2}; + return Manifold_3{make_causal_vertices<3>(vertices, timevalues)}; + } + + [[nodiscard]] auto make_44_move_manifold() -> Manifold_3 + { + vector vertices{ + Point_t<3>{ 0, 0, 0}, + Point_t<3>{ INV_SQRT_2, 0, INV_SQRT_2}, + Point_t<3>{ 0, INV_SQRT_2, INV_SQRT_2}, + Point_t<3>{-INV_SQRT_2, 0, INV_SQRT_2}, + Point_t<3>{ 0, -INV_SQRT_2, INV_SQRT_2}, + Point_t<3>{ 0, 0, 2} + }; + vector const timevalues{0, 1, 1, 1, 1, 2}; + return Manifold_3{make_causal_vertices<3>(vertices, timevalues), 0, 1}; + } + + void check_applied_move(Manifold_3 const& before, Manifold_3 const& after, + move_tracker::move_type const move) + { + CHECK(before.is_correct()); + CHECK(after.is_correct()); + CHECK(ergodic_moves::check_move(before, after, move)); + } +} // namespace -SCENARIO("Apply an ergodic move to 2+1 manifolds" * +SCENARIO("apply_move forwards deterministic ergodic moves" * doctest::test_suite("apply")) { - GIVEN("A 2+1 dimensional spherical manifold.") + GIVEN("A minimal manifold and the null move") { - auto constexpr desired_simplices = 9600; - auto constexpr desired_timeslices = 7; - manifolds::Manifold_3 manifold(desired_simplices, desired_timeslices); - REQUIRE(manifold.is_correct()); - // Copy of manifold - auto manifold_before = manifold; - WHEN("A null move is applied to the manifold.") + auto const before = make_23_move_manifold(); + WHEN("apply_move invokes the null move") { - spdlog::debug("Applying null move to manifold.\n"); - if (auto result = apply_move(manifold, ergodic_moves::null_move); result) + auto result = apply_move(before, ergodic_moves::null_move); + REQUIRE(result.has_value()); + auto const after = std::move(result).value(); + THEN("the result equals the source manifold") { - manifold = result.value(); - } - else - { - spdlog::debug("{}", result.error()); - REQUIRE(result.has_value()); - } - THEN("The resulting manifold is valid and unchanged.") - { - CHECK(manifold.is_valid()); - CHECK_EQ(manifold_before.simplices(), manifold.simplices()); - CHECK_EQ(manifold_before.faces(), manifold.faces()); - CHECK_EQ(manifold_before.edges(), manifold.edges()); - CHECK_EQ(manifold_before.vertices(), manifold.vertices()); - // Human verification - fmt::print("Old manifold.\n"); - manifold_before.print_details(); - fmt::print("New manifold after null move:\n"); - manifold.print_details(); + CHECK(after.is_correct()); + CHECK_EQ(after.delaunay_snapshot(), before.delaunay_snapshot()); } } - WHEN("A (2,3) move is applied to the manifold.") + } + + GIVEN("A minimal manifold supporting a (2,3) move") + { + auto const before = make_23_move_manifold(); + cdt::Random random{92}; + CAPTURE(random.seed()); + WHEN("apply_move invokes the (2,3) move") { - spdlog::debug("Applying (2,3) move to manifold.\n"); - if (auto result = apply_move(manifold, ergodic_moves::do_23_move); result) - { - manifold = result.value(); - } - else - { - spdlog::debug("{}", result.error()); - REQUIRE(result.has_value()); - } - THEN("The resulting manifold has the applied move.") - { - CHECK(ergodic_moves::check_move(manifold_before, manifold, - move_tracker::move_type::TWO_THREE)); - // Human verification - fmt::print("Old manifold.\n"); - manifold_before.print_details(); - fmt::print("New manifold after (2,3) move:\n"); - manifold.print_details(); - } + auto result = + apply_move(before, &ergodic_moves::do_23_move, random); + REQUIRE(result.has_value()); + auto const after = std::move(result).value(); + THEN("the exact (2,3) transition is returned") + { check_applied_move(before, after, move_tracker::move_type::TWO_THREE); } } - WHEN("A (3,2) move is applied to the manifold.") + } + + GIVEN("A minimal manifold supporting a (3,2) move") + { + cdt::Random random{92}; + CAPTURE(random.seed()); + auto setup = ergodic_moves::do_23_move(make_23_move_manifold(), random); + REQUIRE(setup.has_value()); + auto const before = std::move(setup).value(); + WHEN("apply_move invokes the inverse (3,2) move") { - spdlog::debug("Applying (3,2) move to manifold.\n"); - if (auto result = apply_move(manifold, ergodic_moves::do_32_move); result) - { - manifold = result.value(); - } - else - { - spdlog::debug("{}", result.error()); - // Stop further tests - REQUIRE(result.has_value()); - } - THEN("The resulting manifold has the applied move.") - { - CHECK(ergodic_moves::check_move(manifold_before, manifold, - move_tracker::move_type::THREE_TWO)); - // Human verification - fmt::print("Old manifold.\n"); - manifold_before.print_details(); - fmt::print("New manifold after (3,2) move:\n"); - manifold.print_details(); - } + auto result = + apply_move(before, &ergodic_moves::do_32_move, random); + REQUIRE(result.has_value()); + auto const after = std::move(result).value(); + THEN("the exact (3,2) transition is returned") + { check_applied_move(before, after, move_tracker::move_type::THREE_TWO); } } - WHEN("A (2,6) move is applied to the manifold.") + } + + GIVEN("A minimal manifold supporting a (2,6) move") + { + auto const before = make_26_move_manifold(); + cdt::Random random{92}; + CAPTURE(random.seed()); + WHEN("apply_move invokes the (2,6) move") { - spdlog::debug("Applying (2,6) move to manifold.\n"); - if (auto result = apply_move(manifold, ergodic_moves::do_26_move); result) - { - manifold = result.value(); - } - else - { - spdlog::debug("{}", result.error()); - // Stop further tests - REQUIRE(result.has_value()); - } - THEN("The resulting manifold has the applied move.") - { - CHECK(ergodic_moves::check_move(manifold_before, manifold, - move_tracker::move_type::TWO_SIX)); - // Human verification - fmt::print("Old manifold.\n"); - manifold_before.print_details(); - fmt::print("New manifold after (2,6) move:\n"); - manifold.print_details(); - } + auto result = + apply_move(before, &ergodic_moves::do_26_move, random); + REQUIRE(result.has_value()); + auto const after = std::move(result).value(); + THEN("the exact (2,6) transition is returned") + { check_applied_move(before, after, move_tracker::move_type::TWO_SIX); } } - WHEN("A (6,2) move is applied to the manifold.") + } + + GIVEN("A minimal manifold supporting a (6,2) move") + { + cdt::Random random{92}; + CAPTURE(random.seed()); + auto setup = ergodic_moves::do_26_move(make_26_move_manifold(), random); + REQUIRE(setup.has_value()); + auto const before = std::move(setup).value(); + WHEN("apply_move invokes the inverse (6,2) move") { - spdlog::debug("Applying (6,2) move to manifold.\n"); - auto result = apply_move(manifold, ergodic_moves::do_62_move); - if (result) - { - manifold = result.value(); - THEN("The resulting manifold has the applied move.") - { - CHECK(ergodic_moves::check_move(manifold_before, manifold, - move_tracker::move_type::SIX_TWO)); - // Human verification - fmt::print("Old manifold.\n"); - manifold_before.print_details(); - fmt::print("New manifold after (6,2) move:\n"); - manifold.print_details(); - } - } - else - { - spdlog::warn("Cannot apply (6,2) move: {}", result.error()); - THEN("Move unavailability is reported without mutating the manifold.") - { - CHECK_FALSE(result.error().empty()); - CHECK(manifold.is_correct()); - CHECK_EQ(manifold.simplices(), manifold_before.simplices()); - } - } + auto result = + apply_move(before, &ergodic_moves::do_62_move, random); + REQUIRE(result.has_value()); + auto const after = std::move(result).value(); + THEN("the exact (6,2) transition is returned") + { check_applied_move(before, after, move_tracker::move_type::SIX_TWO); } } - WHEN("A (4,4) move is applied to the manifold.") + } + + GIVEN("A minimal manifold supporting a (4,4) move") + { + auto const before = make_44_move_manifold(); + cdt::Random random{92}; + CAPTURE(random.seed()); + WHEN("apply_move invokes the (4,4) move") { - spdlog::debug("Applying (4,4) move to manifold.\n"); - auto result = apply_move(manifold, ergodic_moves::do_44_move); - if (result) - { - manifold = result.value(); - THEN("The resulting manifold has the applied move.") - { - CHECK(ergodic_moves::check_move(manifold_before, manifold, - move_tracker::move_type::FOUR_FOUR)); - // Human verification - fmt::print("Old manifold.\n"); - manifold_before.print_details(); - fmt::print("New manifold after (4,4) move:\n"); - manifold.print_details(); - } - } - else - { - spdlog::debug("{}", result.error()); - THEN("Move unavailability is reported without mutating the manifold.") - { - CHECK_FALSE(result.error().empty()); - CHECK(manifold.is_correct()); - CHECK_EQ(manifold.simplices(), manifold_before.simplices()); - } - } + auto result = + apply_move(before, &ergodic_moves::do_44_move, random); + REQUIRE(result.has_value()); + auto const after = std::move(result).value(); + THEN("the exact (4,4) transition is returned") + { check_applied_move(before, after, move_tracker::move_type::FOUR_FOUR); } } } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 795c652661..19354ed677 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -12,6 +12,7 @@ add_executable( Move_always_test.cpp Move_command_test.cpp Move_tracker_test.cpp + Random_test.cpp Runtime_config_test.cpp S3Action_test.cpp Settings_test.cpp @@ -26,25 +27,30 @@ target_link_libraries( PRIVATE project_options project_warnings date::date-tz + doctest::doctest fmt::fmt-header-only TBB::tbb CGAL::CGAL) +# Compile Random.hpp through only the repository-owned project boundary so PCG +# cannot remain available solely through another vcpkg target's include path. +add_library(CDT_random_header_compile_contract OBJECT Random_header_consumer.cpp) +target_link_libraries( + CDT_random_header_compile_contract + PRIVATE project_options project_warnings) + +# Keep the issue #105 before/after diagnostic buildable without registering it +# as a correctness test. Run it through `just benchmark-rng`. +add_executable(CDT_rng_benchmark Random_benchmark.cpp) +target_compile_features(CDT_rng_benchmark PRIVATE cxx_std_23) +target_link_libraries( + CDT_rng_benchmark + PRIVATE project_options + project_warnings + date::date-tz + fmt::fmt-header-only + spdlog::spdlog_header_only) + # Run unit tests add_test(NAME cdt-unit-tests COMMAND $) - -# Keep deterministic regression-oracle coverage in the supported smoke suite. -add_test( - NAME cdt-function-ref-tests - COMMAND $ --test-suite=function_ref) -add_test( - NAME cdt-foliated-triangulation-tests - COMMAND $ --test-suite=foliated_triangulation) -add_test( - NAME cdt-utilities-tests - COMMAND $ --test-suite=utilities) -set_tests_properties( - cdt-function-ref-tests - cdt-foliated-triangulation-tests - cdt-utilities-tests - PROPERTIES LABELS "full-suite-duplicate" TIMEOUT 60) +set_tests_properties(cdt-unit-tests PROPERTIES LABELS "unit" TIMEOUT 180) diff --git a/tests/Ergodic_moves_3_test.cpp b/tests/Ergodic_moves_3_test.cpp index 80f7c66632..e6932a2002 100644 --- a/tests/Ergodic_moves_3_test.cpp +++ b/tests/Ergodic_moves_3_test.cpp @@ -24,6 +24,56 @@ static inline std::floating_point auto constexpr SQRT_2 = std::numbers::sqrt2_v; static inline std::floating_point auto constexpr INV_SQRT_2 = 1 / SQRT_2; +namespace +{ + struct ExpectedGeometryDelta + { + Int_precision n3; + Int_precision n3_31; + Int_precision n3_13; + Int_precision n3_31_13; + Int_precision n3_22; + Int_precision n2; + Int_precision n1; + Int_precision n1_tl; + Int_precision n1_sl; + Int_precision n0; + }; + + [[nodiscard]] auto constexpr expected_geometry_delta( + move_tracker::move_type const move) -> ExpectedGeometryDelta + { + using enum move_tracker::move_type; + switch (move) + { + case TWO_THREE: return {1, 0, 0, 0, 1, 2, 1, 1, 0, 0}; + case THREE_TWO: return {-1, 0, 0, 0, -1, -2, -1, -1, 0, 0}; + case TWO_SIX: return {4, 2, 2, 4, 0, 8, 5, 2, 3, 1}; + case SIX_TWO: return {-4, -2, -2, -4, 0, -8, -5, -2, -3, -1}; + case FOUR_FOUR: return {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + } + return {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + } + + void check_geometry_delta(Manifold_3 const& before, Manifold_3 const& after, + move_tracker::move_type const move) + { + auto const expected = expected_geometry_delta(move); + CHECK_EQ(after.N3() - before.N3(), expected.n3); + CHECK_EQ(after.N3_31() - before.N3_31(), expected.n3_31); + CHECK_EQ(after.N3_13() - before.N3_13(), expected.n3_13); + CHECK_EQ(after.N3_31_13() - before.N3_31_13(), expected.n3_31_13); + CHECK_EQ(after.N3_22() - before.N3_22(), expected.n3_22); + CHECK_EQ(after.N2() - before.N2(), expected.n2); + CHECK_EQ(after.N1() - before.N1(), expected.n1); + CHECK_EQ(after.N1_TL() - before.N1_TL(), expected.n1_tl); + CHECK_EQ(after.N1_SL() - before.N1_SL(), expected.n1_sl); + CHECK_EQ(after.N0() - before.N0(), expected.n0); + CHECK_EQ(after.max_time(), before.max_time()); + CHECK_EQ(after.min_time(), before.min_time()); + } +} // namespace + SCENARIO("Use check_move to validate successful move" * doctest::test_suite("ergodic")) { @@ -38,14 +88,16 @@ SCENARIO("Use check_move to validate successful move" * Point_t<3>{ SQRT_2, SQRT_2, 0} }; vector timevalues{1, 1, 1, 2, 2}; - auto causal_vertices = make_causal_vertices<3>(vertices, timevalues); - Manifold_3 manifold(causal_vertices); + auto causal_vertices = make_causal_vertices<3>(vertices, timevalues); + Manifold_3 manifold(causal_vertices); + cdt::Random random{92}; + CAPTURE(random.seed()); WHEN("A correct (2,3) move is performed.") { spdlog::debug("When a correct (2,3) move is performed.\n"); // Copy manifold auto manifold_before = manifold; - if (auto result = ergodic_moves::do_23_move(manifold); result) + if (auto result = ergodic_moves::do_23_move(manifold, random); result) { manifold = result.value(); } @@ -67,6 +119,8 @@ SCENARIO("Use check_move to validate successful move" * // CHECK(manifold.is_delaunay()); THEN("check_move returns true") { + check_geometry_delta(manifold_before, manifold, + move_tracker::move_type::TWO_THREE); CHECK(ergodic_moves::check_move(manifold_before, manifold, move_tracker::move_type::TWO_THREE)); } @@ -90,8 +144,10 @@ SCENARIO( Point_t<3>{ SQRT_2, SQRT_2, 0} }; vector timevalues{1, 1, 1, 2, 2}; - auto causal_vertices = make_causal_vertices<3>(vertices, timevalues); - Manifold_3 manifold(causal_vertices); + auto causal_vertices = make_causal_vertices<3>(vertices, timevalues); + Manifold_3 manifold(causal_vertices); + cdt::Random random{92}; + CAPTURE(random.seed()); REQUIRE(manifold.is_correct()); REQUIRE_EQ(manifold.vertices(), 5); @@ -108,7 +164,7 @@ SCENARIO( spdlog::debug("When a (2,3) move is performed.\n"); // Copy manifold auto manifold_before = manifold; - if (auto result = ergodic_moves::do_23_move(manifold); result) + if (auto result = ergodic_moves::do_23_move(manifold, random); result) { manifold = result.value(); } @@ -120,6 +176,8 @@ SCENARIO( } THEN("The move is correct and the manifold invariants are maintained") { + check_geometry_delta(manifold_before, manifold, + move_tracker::move_type::TWO_THREE); CHECK(ergodic_moves::check_move(manifold_before, manifold, move_tracker::move_type::TWO_THREE)); // Manual check @@ -133,16 +191,13 @@ SCENARIO( CHECK_EQ(manifold.N1_SL(), 4); CHECK_EQ(manifold.N1_TL(), 6); // CHECK(manifold.is_delaunay()); - // Human-readable output - manifold.print_details(); - manifold.print_cells(); } } WHEN("A (3,2) move is performed") { spdlog::debug("When a (3,2) move is performed.\n"); // First, do a (2,3) move to set up the manifold - if (auto start = ergodic_moves::do_23_move(manifold); start) + if (auto start = ergodic_moves::do_23_move(manifold, random); start) { manifold = start.value(); } @@ -167,7 +222,7 @@ SCENARIO( // Copy manifold auto manifold_before = manifold; // Do move - if (auto result = ergodic_moves::do_32_move(manifold); result) + if (auto result = ergodic_moves::do_32_move(manifold, random); result) { manifold = result.value(); } @@ -179,6 +234,8 @@ SCENARIO( } THEN("The move is correct and the manifold invariants are maintained") { + check_geometry_delta(manifold_before, manifold, + move_tracker::move_type::THREE_TWO); CHECK(ergodic_moves::check_move(manifold_before, manifold, move_tracker::move_type::THREE_TWO)); // Manual check @@ -192,14 +249,11 @@ SCENARIO( CHECK_EQ(manifold.N1_SL(), 4); CHECK_EQ(manifold.N1_TL(), 5); CHECK(manifold.is_delaunay()); - // Human-readable output - manifold.print_details(); - manifold.print_cells(); } } WHEN("An improperly prepared (3,2) move is performed") { - auto result = ergodic_moves::do_32_move(manifold); + auto result = ergodic_moves::do_32_move(manifold, random); THEN("The move is not performed") { CHECK_FALSE(result); @@ -224,8 +278,10 @@ SCENARIO( Point_t<3>{RADIUS_2, RADIUS_2, RADIUS_2} }; vector timevalues{0, 1, 1, 1, 2}; - auto causal_vertices = make_causal_vertices<3>(vertices, timevalues); - Manifold_3 manifold(causal_vertices); + auto causal_vertices = make_causal_vertices<3>(vertices, timevalues); + Manifold_3 manifold(causal_vertices); + cdt::Random random{92}; + CAPTURE(random.seed()); REQUIRE(manifold.is_correct()); REQUIRE_EQ(manifold.vertices(), 5); @@ -243,7 +299,7 @@ SCENARIO( { spdlog::debug("When a (2,6) move is proposed.\n"); auto const manifold_before = manifold; - auto result = ergodic_moves::do_26_move(manifold); + auto result = ergodic_moves::do_26_move(manifold, random); if (!result) { spdlog::debug("The (2,6) move failed.\n"); } REQUIRE(result.has_value()); @@ -258,6 +314,8 @@ SCENARIO( manifold = std::move(result).value(); THEN("The move is correct and the manifold invariants are maintained") { + check_geometry_delta(manifold_before, manifold, + move_tracker::move_type::TWO_SIX); CHECK(ergodic_moves::check_move(manifold_before, manifold, move_tracker::move_type::TWO_SIX)); // Manual check @@ -275,13 +333,6 @@ SCENARIO( CHECK_EQ(manifold.N1_SL(), 6); // +3 spacelike edges CHECK_EQ(manifold.N1_TL(), 8); // +2 timelike edges CHECK(manifold.is_delaunay()); - // Human-readable output - fmt::print("Manifold before (2,6):\n"); - manifold_before.print_details(); - manifold_before.print_cells(); - fmt::print("Manifold after (2,6):\n"); - manifold.print_details(); - manifold.print_cells(); } } } @@ -289,7 +340,7 @@ SCENARIO( { spdlog::debug("When a (6,2) move is proposed.\n"); // First, do a (2,6) move to set up the manifold - if (auto start = ergodic_moves::do_26_move(manifold); start) + if (auto start = ergodic_moves::do_26_move(manifold, random); start) { manifold = start.value(); } @@ -316,8 +367,8 @@ SCENARIO( // Copy manifold auto const manifold_before = manifold; - auto result = ergodic_moves::do_62_move(manifold); - if (!result) { spdlog::info("The (6,2) move failed.\n"); } + auto result = ergodic_moves::do_62_move(manifold, random); + if (!result) { spdlog::debug("The (6,2) move failed.\n"); } REQUIRE(result.has_value()); THEN("The proposal leaves the source manifold unchanged") @@ -331,6 +382,8 @@ SCENARIO( manifold = std::move(result).value(); THEN("The move is correct and the manifold invariants are maintained") { + check_geometry_delta(manifold_before, manifold, + move_tracker::move_type::SIX_TWO); // Check the move CHECK(ergodic_moves::check_move(manifold_before, manifold, move_tracker::move_type::SIX_TWO)); @@ -350,19 +403,12 @@ SCENARIO( CHECK_EQ(manifold.N1_SL(), 3); CHECK_EQ(manifold.N1_TL(), 6); CHECK(manifold.is_delaunay()); - // Human-readable output - fmt::print("Manifold before (6,2):\n"); - manifold_before.print_details(); - manifold_before.print_cells(); - fmt::print("Manifold after (6,2):\n"); - manifold.print_details(); - manifold.print_cells(); } } } WHEN("An improperly prepared (6,2) move is performed") { - auto result = ergodic_moves::do_62_move(manifold); + auto result = ergodic_moves::do_62_move(manifold, random); THEN("The move is not performed") { CHECK_FALSE(result); @@ -387,8 +433,10 @@ SCENARIO("Perform ergodic moves on the minimal manifold necessary (4,4) moves" * Point_t<3>{ 0, 0, 2} }; vector timevalues{0, 1, 1, 1, 1, 2}; - auto causal_vertices = make_causal_vertices<3>(vertices, timevalues); - Manifold_3 manifold(causal_vertices, 0, 1); + auto causal_vertices = make_causal_vertices<3>(vertices, timevalues); + Manifold_3 manifold(causal_vertices, 0, 1); + cdt::Random random{92}; + CAPTURE(random.seed()); // Verify we have 4 vertices, 4 edges, 4 faces, and 4 simplices REQUIRE_EQ(manifold.vertices(), 6); REQUIRE_EQ(manifold.edges(), 13); @@ -409,12 +457,8 @@ SCENARIO("Perform ergodic moves on the minimal manifold necessary (4,4) moves" * { spdlog::debug("When a (4,4) move is proposed.\n"); auto const manifold_before = manifold; - // Human verification - fmt::print("Manifold before (4,4):\n"); - manifold_before.print_details(); - manifold_before.print_cells(); - auto result = ergodic_moves::do_44_move(manifold); - if (!result) { spdlog::info("The (4,4) move failed.\n"); } + auto result = ergodic_moves::do_44_move(manifold, random); + if (!result) { spdlog::debug("The (4,4) move failed.\n"); } REQUIRE(result.has_value()); THEN("The proposal leaves the source manifold unchanged") @@ -426,12 +470,11 @@ SCENARIO("Perform ergodic moves on the minimal manifold necessary (4,4) moves" * AND_WHEN("The proposed value is accepted") { manifold = std::move(result).value(); - fmt::print("Manifold after (4,4):\n"); - manifold.print_details(); - manifold.print_cells(); THEN("The move is correct and the manifold invariants are maintained") { + check_geometry_delta(manifold_before, manifold, + move_tracker::move_type::FOUR_FOUR); // Check the move CHECK(ergodic_moves::check_move(manifold_before, manifold, move_tracker::move_type::FOUR_FOUR)); @@ -531,10 +574,11 @@ SCENARIO("Rejected topology moves preserve the source value" * { Manifold_3 const source; auto const before = source; + cdt::Random random{92}; WHEN("A (2,6) move is proposed") { - auto const result = ergodic_moves::do_26_move(source); + auto const result = ergodic_moves::do_26_move(source, random); THEN("The move is rejected without changing the source") { @@ -548,7 +592,7 @@ SCENARIO("Rejected topology moves preserve the source value" * } WHEN("A (4,4) move is proposed") { - auto const result = ergodic_moves::do_44_move(source); + auto const result = ergodic_moves::do_44_move(source, random); THEN("The move is rejected without changing the source") { diff --git a/tests/Foliated_triangulation_test.cpp b/tests/Foliated_triangulation_test.cpp index 191e249b8e..883be83f24 100644 --- a/tests/Foliated_triangulation_test.cpp +++ b/tests/Foliated_triangulation_test.cpp @@ -24,6 +24,8 @@ static_assert(std::is_nothrow_swappable_v); static_assert(std::is_nothrow_move_constructible_v); static_assert(std::is_nothrow_move_assignable_v); +using Causal_vertices_3_t = Causal_vertices_t<3>; + static inline auto constexpr RADIUS_2 = 2.0 * std::numbers::inv_sqrt3_v; static inline std::floating_point auto constexpr SQRT_2 = std::numbers::sqrt2_v; @@ -82,19 +84,19 @@ SCENARIO("FoliatedTriangulation special member and swap properties" * THEN("It is constructible from parameters.") { REQUIRE(is_constructible_v); + Int_precision, cdt::Random, double, double>); spdlog::debug("It is constructible from parameters.\n"); } THEN("It is constructible from Causal_vertices.") { REQUIRE( - is_constructible_v>); + is_constructible_v); spdlog::debug("It is constructible from Causal_vertices.\n"); } THEN("It is constructible from Causal_vertices and INITIAL_RADIUS.") { - REQUIRE(is_constructible_v, double>); + REQUIRE(is_constructible_v); spdlog::debug( "It is constructible from Causal_vertices and INITIAL_RADIUS.\n"); } @@ -102,8 +104,8 @@ SCENARIO("FoliatedTriangulation special member and swap properties" * "It is constructible from Causal_vertices, INITIAL_RADIUS, and " "RADIAL_SEPARATION.") { - REQUIRE(is_constructible_v, double, double>); + REQUIRE(is_constructible_v); spdlog::debug( "It is constructible from Causal_vertices, INITIAL_RADIUS, and " "RADIAL_SEPARATION.\n"); @@ -165,16 +167,8 @@ SCENARIO("FoliatedTriangulation free functions" * vector timevalues{1, 1, 1, 2}; auto vertices = make_causal_vertices<3>(Vertices, timevalues); FoliatedTriangulation_3 triangulation(vertices); - auto snapshot = triangulation.delaunay_snapshot(); - auto all_vertices = collect_vertices<3>(snapshot); - auto all_cells = collect_cells<3>(snapshot); - auto print = [&snapshot](auto& vertex) { - fmt::print( - "Vertex: ({}) Timevalue: {} is a vertex: {} and is " - "infinite: {}\n", - utilities::point_to_str(vertex->point()), vertex->info(), - snapshot.tds().is_vertex(vertex), snapshot.is_infinite(vertex)); - }; + auto snapshot = triangulation.delaunay_snapshot(); + auto all_cells = collect_cells<3>(snapshot); REQUIRE(triangulation.is_initialized()); WHEN("check_vertices() is called.") @@ -185,28 +179,14 @@ SCENARIO("FoliatedTriangulation free functions" * WHEN("check_cells() is called.") { THEN("Cells are correctly classified.") - { - CHECK(foliated_triangulations::check_cells<3>(snapshot)); - // Human verification - triangulation.print_cells(); - } - } - - WHEN("We print a cell in the triangulation.") - { - THEN("A cell is printed correctly.") - { foliated_triangulations::print_cell<3>(all_cells.at(0)); } + { CHECK(foliated_triangulations::check_cells<3>(snapshot)); } } WHEN("We ask for a container of vertices given a container of cells.") { auto vertices_from_cells = get_vertices_from_cells<3>(all_cells); THEN("We get back the correct number of vertices.") - { - REQUIRE_EQ(vertices_from_cells.size(), 4); - // Human verification - ranges::for_each(vertices_from_cells, print); - } + { REQUIRE_EQ(vertices_from_cells.size(), 4); } } } GIVEN( @@ -218,7 +198,7 @@ SCENARIO("FoliatedTriangulation free functions" * auto constexpr initial_radius = 3.0; auto constexpr foliation_spacing = 2.0; FoliatedTriangulation_3 const triangulation( - desired_simplices, desired_timeslices, initial_radius, + desired_simplices, desired_timeslices, cdt::Random{92}, initial_radius, foliation_spacing); THEN("The triangulation is initialized correctly.") { REQUIRE(triangulation.is_initialized()); } @@ -226,11 +206,6 @@ SCENARIO("FoliatedTriangulation free functions" * { REQUIRE_EQ(triangulation.initial_radius(), initial_radius); REQUIRE_EQ(triangulation.foliation_spacing(), foliation_spacing); - // Human verification - fmt::print( - "The triangulation has an initial radius of {} and a radial " - "separation of {}\n", - initial_radius, foliation_spacing); } THEN("Each vertex has a valid timevalue.") { @@ -238,13 +213,6 @@ SCENARIO("FoliatedTriangulation free functions" * for (auto const& vertex : collect_vertices<3>(snapshot)) { CHECK(triangulation.does_vertex_radius_match_timevalue(vertex)); - fmt::print( - "Vertex ({}) with timevalue of {} has a squared radius of {} and a " - "squared expected radius of {} with an expected timevalue of {}.\n", - utilities::point_to_str(vertex->point()), vertex->info(), - squared_radius<3>(vertex), - std::pow(triangulation.expected_radius(vertex), 2), - triangulation.expected_timevalue(vertex)); } } } @@ -288,11 +256,6 @@ SCENARIO("FoliatedTriangulation free functions" * { CHECK_EQ(vertex.value()->point(), Point_t<3>{0, 0, 0}); CHECK_EQ(vertex.value()->info(), 1); - // Human verification - fmt::print( - "Point(0,0,0) was found as vertex ({}) with a timevalue of {}.\n", - utilities::point_to_str(vertex.value()->point()), - vertex.value()->info()); } } WHEN("We choose a point not in the triangulation.") @@ -302,8 +265,6 @@ SCENARIO("FoliatedTriangulation free functions" * auto vertex = foliated_triangulations::find_vertex<3>( snapshot, Point_t<3>{3, 3, 3}); REQUIRE_FALSE(vertex); - // Human verification - fmt::print("Point(3,3,3) was not found.\n"); } } WHEN("We check vertices in a cell.") @@ -327,8 +288,6 @@ SCENARIO("FoliatedTriangulation free functions" * auto cell = foliated_triangulations::find_cell<3>( snapshot, v_1.value(), v_2.value(), v_3.value(), v_4.value()); CHECK(cell); - // Human verification - triangulation.print_cells(); } } THEN("The incorrect vertices do not return a cell.") @@ -415,16 +374,14 @@ SCENARIO("FoliatedTriangulation_3 initialization" * REQUIRE_EQ(triangulation.initial_radius(), INITIAL_RADIUS); REQUIRE_EQ(triangulation.foliation_spacing(), FOLIATION_SPACING); REQUIRE(triangulation.is_foliated()); - // Human verification - triangulation.print_cells(); } } WHEN("Constructing the minimum triangulation.") { auto constexpr desired_simplices = 2; auto constexpr desired_timeslices = 2; - FoliatedTriangulation_3 triangulation(desired_simplices, - desired_timeslices); + FoliatedTriangulation_3 triangulation( + desired_simplices, desired_timeslices, cdt::Random{92}); THEN("Triangulation is valid and foliated.") { REQUIRE(triangulation.is_initialized()); } THEN("The triangulation has sensible values.") @@ -437,9 +394,6 @@ SCENARIO("FoliatedTriangulation_3 initialization" * auto simplex_count{triangulation.number_of_finite_cells()}; CHECK_GE(simplex_count, 1); CHECK_LE(simplex_count, 12); - - // Human verification - triangulation.print(); } THEN("The vertices have correct timevalues.") { @@ -449,18 +403,6 @@ SCENARIO("FoliatedTriangulation_3 initialization" * CHECK(triangulation.does_vertex_radius_match_timevalue(vertex)); }; ranges::for_each(snapshot_vertices, check); - // Human verification - auto print = [&triangulation](Vertex_handle_t<3> const& vertex) { - fmt::print( - "Vertex: ({}) Timevalue: {} has a squared radius of {} and " - "a squared expected radius of {} with an expected timevalue of " - "{}.\n", - utilities::point_to_str(vertex->point()), vertex->info(), - squared_radius<3>(vertex), - std::pow(triangulation.expected_radius(vertex), 2), - triangulation.expected_timevalue(vertex)); - }; - ranges::for_each(snapshot_vertices, print); } } WHEN( @@ -472,7 +414,8 @@ SCENARIO("FoliatedTriangulation_3 initialization" * auto constexpr initial_radius = 3.0; auto constexpr radial_factor = 2.0; FoliatedTriangulation_3 const triangulation( - desired_simplices, desired_timeslices, initial_radius, radial_factor); + desired_simplices, desired_timeslices, cdt::Random{92}, + initial_radius, radial_factor); THEN("The triangulation is initialized correctly.") { REQUIRE(triangulation.is_initialized()); } THEN("The initial radius and radial separation are correct.") @@ -490,7 +433,8 @@ SCENARIO("FoliatedTriangulation_3 initialization" * auto constexpr initial_radius = 1.5; auto constexpr radial_factor = 1.1; FoliatedTriangulation_3 const triangulation( - desired_simplices, desired_timeslices, initial_radius, radial_factor); + desired_simplices, desired_timeslices, cdt::Random{92}, + initial_radius, radial_factor); THEN("The triangulation is initialized correctly.") { REQUIRE(triangulation.is_initialized()); } THEN("The initial radius and radial separation are correct.") @@ -503,19 +447,14 @@ SCENARIO("FoliatedTriangulation_3 initialization" * { auto constexpr desired_simplices = 6400; auto constexpr desired_timeslices = 7; - FoliatedTriangulation_3 const triangulation(desired_simplices, - desired_timeslices); + FoliatedTriangulation_3 const triangulation( + desired_simplices, desired_timeslices, cdt::Random{92}); THEN("Triangulation is valid and foliated.") { REQUIRE(triangulation.is_initialized()); } THEN("The triangulation has sensible values.") - { - REQUIRE_EQ(triangulation.min_time(), 1); - // Human verification - triangulation.print(); - } + { REQUIRE_EQ(triangulation.min_time(), 1); } THEN("Data members are correctly populated.") { - triangulation.print(); // Every cell is classified as (3,1), (2,2), or (1,3) CHECK_EQ(triangulation.number_of_finite_cells(), triangulation.number_of_three_one_cells() + @@ -542,17 +481,6 @@ SCENARIO("FoliatedTriangulation_3 initialization" * CHECK(!classify_edge<3>(edge)); }; ranges::for_each(spacelike_edges, check_spacelike); - // Human verification - fmt::print("There are {} edges.\n", - triangulation.number_of_finite_edges()); - fmt::print("There are {} timelike edges and {} spacelike edges.\n", - triangulation.N1_TL(), triangulation.N1_SL()); - fmt::print( - "There are {} vertices with a max timevalue of {} and a min " - "timevalue of {}.\n", - triangulation.number_of_vertices(), triangulation.max_time(), - triangulation.min_time()); - triangulation.print_volume_per_timeslice(); } } } @@ -566,8 +494,8 @@ SCENARIO("FoliatedTriangulation_3 copying" * { auto constexpr desired_simplices = 6400; auto constexpr desired_timeslices = 7; - FoliatedTriangulation_3 triangulation(desired_simplices, - desired_timeslices); + FoliatedTriangulation_3 triangulation(desired_simplices, desired_timeslices, + cdt::Random{92}); WHEN("It is copied") { auto ft2 = triangulation; @@ -603,7 +531,8 @@ SCENARIO("FoliatedTriangulation_3 moving" * { auto constexpr desired_simplices = 64; auto constexpr desired_timeslices = 4; - FoliatedTriangulation_3 source(desired_simplices, desired_timeslices); + FoliatedTriangulation_3 source(desired_simplices, desired_timeslices, + cdt::Random{92}); auto const expected_cells = source.number_of_finite_cells(); auto const expected_vertices = source.number_of_vertices(); auto const expected_three_one = source.number_of_three_one_cells(); @@ -629,7 +558,7 @@ SCENARIO("FoliatedTriangulation_3 moving" * WHEN("It is move assigned over another foliated triangulation.") { FoliatedTriangulation_3 assigned(desired_simplices * 2, - desired_timeslices); + desired_timeslices, cdt::Random{93}); auto const replaced_cells = assigned.number_of_finite_cells(); assigned = std::move(source); @@ -647,7 +576,8 @@ SCENARIO("FoliatedTriangulation_3 moving" * CHECK(source.is_initialized()); CHECK_EQ(source.number_of_finite_cells(), replaced_cells); - source = FoliatedTriangulation_3{desired_simplices, desired_timeslices}; + source = FoliatedTriangulation_3{desired_simplices, desired_timeslices, + cdt::Random{92}}; CHECK(source.is_initialized()); } } @@ -672,25 +602,17 @@ SCENARIO("Detecting and fixing problems with vertices and cells" * auto vertices = make_causal_vertices<3>(Vertices, timevalues); FoliatedTriangulation_3 triangulation(vertices); THEN("No errors in the vertices are detected.") - { - CHECK(triangulation.check_all_vertices()); - // Human verification - triangulation.print_vertices(); - } + { CHECK(triangulation.check_all_vertices()); } THEN("No errors in the simplex are detected.") { auto snapshot = triangulation.delaunay_snapshot(); CHECK(triangulation.is_correct()); CHECK_FALSE(check_timevalues<3>(snapshot)); - // Human verification - triangulation.print_cells(); } THEN("No errors in the triangulation foliation are detected") { auto candidate = triangulation.delaunay_snapshot(); CHECK_FALSE(foliated_triangulations::fix_timevalues<3>(candidate)); - // Human verification - utilities::print_delaunay(candidate); } AND_WHEN("Vertices in an owning snapshot are mis-labelled.") { @@ -758,7 +680,6 @@ SCENARIO("Detecting and fixing problems with vertices and cells" * { CHECK_FALSE(triangulation.fix_vertices()); CHECK(triangulation.is_initialized()); - triangulation.print_cells(); } } WHEN("Constructing a triangulation with an incorrect low value vertex.") @@ -776,7 +697,6 @@ SCENARIO("Detecting and fixing problems with vertices and cells" * { CHECK_FALSE(triangulation.fix_vertices()); CHECK(triangulation.is_initialized()); - triangulation.print_cells(); } } WHEN( @@ -796,14 +716,12 @@ SCENARIO("Detecting and fixing problems with vertices and cells" * { CHECK_FALSE(triangulation.fix_vertices()); CHECK(triangulation.is_initialized()); - triangulation.print_cells(); } AND_THEN("The cell type is correct.") { CHECK_FALSE(triangulation.fix_vertices()); CHECK_FALSE(triangulation.fix_cells()); CHECK(triangulation.is_initialized()); - triangulation.print_cells(); } } WHEN( @@ -824,8 +742,6 @@ SCENARIO("Detecting and fixing problems with vertices and cells" * auto snapshot = triangulation.delaunay_snapshot(); auto cell = snapshot.finite_cells_begin(); CHECK_EQ(expected_cell_type<3>(cell), Cell_type::ACAUSAL); - // Human verification - triangulation.print_cells(); } } WHEN("Constructing a triangulation with an unfixable vertex.") @@ -850,11 +766,6 @@ SCENARIO("Detecting and fixing problems with vertices and cells" * { auto bad_cells = check_timevalues<3>(delaunay_triangulation); CHECK_MESSAGE(bad_cells.has_value(), "No bad cells found."); - if (bad_cells) - { - fmt::print("Bad cells:\n"); - print_cells<3>(bad_cells.value()); - } } AND_THEN("The incorrect vertex can be identified.") { @@ -863,23 +774,15 @@ SCENARIO("Detecting and fixing problems with vertices and cells" * if (bad_cells) { auto bad_vertex = find_bad_vertex<3>(bad_cells->front()); - fmt::print("Bad vertex ({}) has timevalues {}.\n", - utilities::point_to_str(bad_vertex->point()), - bad_vertex->info()); CHECK_EQ(bad_vertex->info(), 3); } } AND_THEN("The triangulation is fixed.") { - fmt::print("Unfixed triangulation:\n"); - triangulation.print_cells(); auto candidate = triangulation.delaunay_snapshot(); CHECK(foliated_triangulations::fix_timevalues<3>(candidate)); triangulation = FoliatedTriangulation_3{std::move(candidate)}; CHECK(triangulation.is_initialized()); - fmt::print("Fixed triangulation:\n"); - auto snapshot = triangulation.delaunay_snapshot(); - print_cells<3>(collect_cells<3>(snapshot)); } } } @@ -905,39 +808,16 @@ SCENARIO("FoliatedTriangulation_3 functions from Delaunay3" * auto causal_vertices = make_causal_vertices<3>(vertices, timevalues); FoliatedTriangulation_3 triangulation(causal_vertices); THEN("The Foliated triangulation is initially wrong.") - { - CHECK_FALSE(triangulation.is_initialized()); - // Human verification -#ifndef NDEBUG - fmt::print("Unfixed triangulation:\n"); - triangulation.print_cells(); -#endif - } + { CHECK_FALSE(triangulation.is_initialized()); } THEN("After being fixed, Delaunay3 functions work as expected.") { // Fix the triangulation CHECK(triangulation.is_fixed()); CHECK_EQ(triangulation.number_of_finite_cells(), 2); - fmt::print("Base Delaunay number of cells: {}\n", - triangulation.number_of_finite_cells()); CHECK_EQ(triangulation.number_of_finite_facets(), 7); - fmt::print("Base Delaunay number of faces: {}\n", - triangulation.number_of_finite_facets()); - triangulation.print_volume_per_timeslice(); CHECK_EQ(triangulation.number_of_finite_edges(), 9); - fmt::print("Base Delaunay number of edges: {}\n", - triangulation.number_of_finite_edges()); - triangulation.print_edges(); CHECK_EQ(triangulation.number_of_vertices(), 5); - fmt::print("Base Delaunay number of vertices: {}\n", - triangulation.number_of_vertices()); CHECK_EQ(triangulation.dimension(), 3); - fmt::print("Base Delaunay dimension is: {}\n", - triangulation.dimension()); - // Human verification -#ifndef NDEBUG - utilities::print_delaunay(triangulation.delaunay_snapshot()); -#endif } } WHEN("Constructing the default triangulation.") diff --git a/tests/Function_ref_test.cpp b/tests/Function_ref_test.cpp index 69b79bad87..a2b4e1f472 100644 --- a/tests/Function_ref_test.cpp +++ b/tests/Function_ref_test.cpp @@ -70,20 +70,17 @@ SCENARIO("Complex lambda operations" * doctest::test_suite("function_ref")) REQUIRE(manifold.is_correct()); WHEN("A lambda is constructed for a move.") { - auto const move23 = [](Manifold_3 const& m) { - return ergodic_moves::do_23_move(m).value(); + auto const move23 = [](Manifold_3 const& m, cdt::Random& random) { + return ergodic_moves::do_23_move(m, random).value(); }; THEN("Running the lambda makes the move.") { - auto const manifold_before = manifold; - auto result = move23(manifold); + auto const manifold_before = manifold; + cdt::Random random{92}; + CAPTURE(random.seed()); + auto result = move23(manifold, random); CHECK(ergodic_moves::check_move(manifold_before, result, move_tracker::move_type::TWO_THREE)); - // Human verification - fmt::print("Manifold properties:\n"); - manifold.print_details(); - fmt::print("Moved manifold properties:\n"); - result.print_details(); } } } @@ -105,22 +102,19 @@ SCENARIO("Function_ref operations" * doctest::test_suite("function_ref")) auto manifold = make_23_move_manifold(); REQUIRE(manifold.is_correct()); boost::compat::function_ref const - complex_ref(ergodic_moves::do_23_move); + ergodic_moves::Manifold const&, cdt::Random&)> const + complex_ref(ergodic_moves::do_23_move); WHEN("The function_ref is invoked.") { - auto const manifold_before = manifold; - auto result = complex_ref(manifold); + auto const manifold_before = manifold; + cdt::Random random{92}; + CAPTURE(random.seed()); + auto result = complex_ref(manifold, random); REQUIRE(result.has_value()); THEN("The move from the function_ref is correct.") { CHECK(ergodic_moves::check_move(manifold_before, result.value(), move_tracker::move_type::TWO_THREE)); - // Human verification - fmt::print("Manifold properties:\n"); - manifold.print_details(); - fmt::print("Moved manifold properties:\n"); - result->print_details(); } } } @@ -128,26 +122,23 @@ SCENARIO("Function_ref operations" * doctest::test_suite("function_ref")) { auto manifold = make_23_move_manifold(); REQUIRE(manifold.is_correct()); - auto const move23 = [](Manifold_3 const& t_manifold) { - return ergodic_moves::do_23_move(t_manifold).value(); + auto const move23 = [](Manifold_3 const& t_manifold, cdt::Random& random) { + return ergodic_moves::do_23_move(t_manifold, random).value(); }; - boost::compat::function_ref const - complex_ref(move23); + boost::compat::function_ref const complex_ref(move23); WHEN("The function_ref is invoked.") { - auto const manifold_before = manifold; - auto result = complex_ref(manifold); + auto const manifold_before = manifold; + cdt::Random random{92}; + CAPTURE(random.seed()); + auto result = complex_ref(manifold, random); THEN( "The move stored in the lambda invoked by the function_ref is " "correct.") { CHECK(ergodic_moves::check_move(manifold_before, result, move_tracker::move_type::TWO_THREE)); - // Human verification - fmt::print("Manifold properties:\n"); - manifold.print_details(); - fmt::print("Moved manifold properties:\n"); - result.print_details(); } } } diff --git a/tests/Geometry_test.cpp b/tests/Geometry_test.cpp index 8b5dac466a..1a8d46e4a0 100644 --- a/tests/Geometry_test.cpp +++ b/tests/Geometry_test.cpp @@ -75,16 +75,11 @@ SCENARIO("3-Geometry classification" * doctest::test_suite("geometry")) { auto constexpr desired_simplices = 72; auto constexpr desired_timeslices = 3; - FoliatedTriangulation_3 const triangulation(desired_simplices, - desired_timeslices); - Geometry_3 geometry(triangulation); + FoliatedTriangulation_3 const triangulation( + desired_simplices, desired_timeslices, cdt::Random{92}); + Geometry_3 geometry(triangulation); THEN("The Delaunay triangulation is described by the geometry.") { - fmt::print("There are {} simplices ...\n", geometry.N3); - fmt::print( - "There are {} (3,1) simplices and {} (2,2) simplices and {} (1,3) " - "simplices.\n", - geometry.N3_31, geometry.N3_22, geometry.N3_13); CHECK_GT(geometry.N3, 2); CHECK_EQ(geometry.N3, static_cast( triangulation.number_of_finite_cells())); @@ -106,20 +101,6 @@ SCENARIO("3-Geometry classification" * doctest::test_suite("geometry")) CHECK_EQ(geometry.N1, geometry.N1_TL + geometry.N1_SL); CHECK_EQ(geometry.N0, static_cast( triangulation.number_of_vertices())); - - // Human verification - fmt::print("There are {} edges.\n", geometry.N1); - fmt::print("There are {} timelike edges and {} spacelike edges.\n", - geometry.N1_TL, geometry.N1_SL); -#ifndef NDEBUG - triangulation.print_cells(); - triangulation.print_edges(); -#endif - fmt::print( - "There are {} vertices with a max timevalue of {} and a min " - "timevalue of {}.\n", - geometry.N0, triangulation.max_time(), triangulation.min_time()); - triangulation.print_volume_per_timeslice(); } } } @@ -150,9 +131,9 @@ SCENARIO("3-Geometry initialization" * doctest::test_suite("geometry")) { auto constexpr desired_simplices = 640; auto constexpr desired_timeslices = 4; - FoliatedTriangulation_3 const triangulation(desired_simplices, - desired_timeslices); - Geometry_3 const geometry(triangulation); + FoliatedTriangulation_3 const triangulation( + desired_simplices, desired_timeslices, cdt::Random{92}); + Geometry_3 const geometry(triangulation); THEN( "The properties of the Delaunay triangulation are saved in geometry " "info.") @@ -177,8 +158,6 @@ SCENARIO("3-Geometry initialization" * doctest::test_suite("geometry")) CHECK_EQ(geometry.N1_TL + geometry.N1_SL, geometry.N1); CHECK_EQ(geometry.N0, static_cast( triangulation.number_of_vertices())); - triangulation.print(); - triangulation.print_volume_per_timeslice(); } } } diff --git a/tests/Manifold_test.cpp b/tests/Manifold_test.cpp index 82505547a1..451f108a6f 100644 --- a/tests/Manifold_test.cpp +++ b/tests/Manifold_test.cpp @@ -23,6 +23,8 @@ static_assert(std::is_nothrow_swappable_v); static_assert(std::is_nothrow_move_constructible_v); static_assert(std::is_nothrow_move_assignable_v); +using Causal_vertices_3_t = Causal_vertices_t<3>; + static inline auto constexpr RADIUS_2 = 2.0 * std::numbers::inv_sqrt3_v; SCENARIO("Manifold special member and swap properties" * @@ -66,25 +68,26 @@ SCENARIO("Manifold special member and swap properties" * Manifold_3, foliated_triangulations::FoliatedTriangulation_3>); spdlog::debug("It is constructible from a FoliatedTriangulation.\n"); } - THEN("It is constructible from 2 parameters.") + THEN("Random initialization requires an explicit owned stream.") { - REQUIRE(is_constructible_v); - spdlog::debug("It is constructible from 2 parameters.\n"); + REQUIRE(is_constructible_v); + REQUIRE_FALSE( + is_constructible_v); } - THEN("It is constructible from 4 parameters.") + THEN("It is constructible from explicit RNG and geometry parameters.") { REQUIRE(is_constructible_v); - spdlog::debug("It is constructible from 4 parameters.\n"); + cdt::Random, double, double>); } THEN("It is constructible from Causal_vertices.") { - REQUIRE(is_constructible_v>); + REQUIRE(is_constructible_v); spdlog::debug("It is constructible from Causal_vertices.\n"); } THEN("It is constructible from Causal_vertices and INITIAL_RADIUS.") { - REQUIRE(is_constructible_v, double>); + REQUIRE(is_constructible_v); spdlog::debug( "It is constructible from Causal_vertices and INITIAL_RADIUS.\n"); } @@ -92,7 +95,7 @@ SCENARIO("Manifold special member and swap properties" * "It is constructible from Causal_vertices, INITIAL_RADIUS, and " "RADIAL_SEPARATION.") { - REQUIRE(is_constructible_v, double, + REQUIRE(is_constructible_v); spdlog::debug( "It is constructible from Causal_vertices, INITIAL_RADIUS, and " @@ -161,13 +164,7 @@ SCENARIO("Manifold free functions" * doctest::test_suite("manifold")) WHEN("The manifold is constructed.") { Manifold_3 const manifold(causal_vertices, 1, 1.0); - THEN("It is correct.") - { - REQUIRE(manifold.is_correct()); - manifold.print(); - manifold.print_details(); - manifold.print_vertices(); - } + THEN("It is correct.") { REQUIRE(manifold.is_correct()); } THEN("We can obtain the vertices from the points.") { auto snapshot = manifold.delaunay_snapshot(); @@ -175,7 +172,6 @@ SCENARIO("Manifold free functions" * doctest::test_suite("manifold")) REQUIRE(v_1); CHECK(v_1.value()->is_valid()); CHECK(snapshot.tds().is_vertex(v_1.value())); - cout << "v_1 contains point " << v_1.value()->point() << '\n'; } THEN("We can obtain the cell from the vertices.") { @@ -196,10 +192,6 @@ SCENARIO("Manifold free functions" * doctest::test_suite("manifold")) // We have to have a valid Cell handle to obtain a tetrahedron auto tetrahedron = snapshot.tetrahedron(cell.value()); CHECK_FALSE(tetrahedron.is_degenerate()); - cout << "Vertex 0 of tetrahedron is " << tetrahedron.vertex(0) << '\n'; - cout << "Vertex 1 of tetrahedron is " << tetrahedron.vertex(1) << '\n'; - cout << "Vertex 2 of tetrahedron is " << tetrahedron.vertex(2) << '\n'; - cout << "Vertex 3 of tetrahedron is " << tetrahedron.vertex(3) << '\n'; } } } @@ -238,8 +230,6 @@ SCENARIO("Manifold functions" * doctest::test_suite("manifold")) { REQUIRE_EQ(manifold.N0(), 4); CHECK(manifold.is_correct()); - // Human verification - manifold.print_vertices(); } } AND_WHEN("Vertices in an owning snapshot are mis-labelled.") @@ -279,8 +269,6 @@ SCENARIO("3-Manifold initialization" * doctest::test_suite("manifold")) auto const& geometry_type = typeid(manifold.get_geometry()).name(); std::string geometry_string{geometry_type}; CHECK_NE(geometry_string.find("Geometry"), std::string::npos); - fmt::print("The Geometry data structure is of type {}\n", - geometry_string); } } WHEN("It is constructed from causal vertices.") @@ -302,8 +290,6 @@ SCENARIO("3-Manifold initialization" * doctest::test_suite("manifold")) auto const& geometry_type = typeid(manifold.get_geometry()).name(); std::string geometry_string{geometry_type}; CHECK_NE(geometry_string.find("Geometry"), std::string::npos); - fmt::print("The Geometry data structure is of type {}\n", - geometry_string); } THEN("The geometry matches the triangulation.") { @@ -320,9 +306,6 @@ SCENARIO("3-Manifold initialization" * doctest::test_suite("manifold")) REQUIRE_EQ(manifold.min_time(), 1); REQUIRE_EQ(manifold.max_time(), 3); REQUIRE(manifold.check_simplices()); - // Human verification - manifold.print(); - manifold.print_volume_per_timeslice(); } } WHEN("It is constructed from a Foliated triangulation.") @@ -348,8 +331,6 @@ SCENARIO("3-Manifold initialization" * doctest::test_suite("manifold")) auto const& geometry_type = typeid(manifold.get_geometry()).name(); std::string geometry_string{geometry_type}; CHECK_NE(geometry_string.find("Geometry"), std::string::npos); - fmt::print("The Geometry data structure is of type {}\n", - geometry_string); } THEN("The geometry matches the triangulation.") { @@ -366,16 +347,14 @@ SCENARIO("3-Manifold initialization" * doctest::test_suite("manifold")) CHECK_EQ(manifold.min_time(), 1); CHECK_EQ(manifold.max_time(), 3); REQUIRE(manifold.check_simplices()); - // Human verification - manifold.print(); - manifold.print_volume_per_timeslice(); } } WHEN("Constructing the minimum size triangulation.") { auto constexpr desired_simplices = 2; auto constexpr desired_timeslices = 2; - Manifold_3 const manifold(desired_simplices, desired_timeslices); + Manifold_3 const manifold(desired_simplices, desired_timeslices, + cdt::Random{92}); THEN("Triangulation is valid.") { REQUIRE(manifold.is_correct()); } THEN("The geometry matches the triangulation.") { @@ -395,16 +374,14 @@ SCENARIO("3-Manifold initialization" * doctest::test_suite("manifold")) // We have all the time values CHECK_EQ(manifold.min_time(), 1); CHECK_EQ(manifold.max_time(), desired_timeslices); - // Human verification - manifold.print(); - manifold.print_volume_per_timeslice(); } } WHEN("Constructing a small triangulation.") { auto constexpr desired_simplices = 640; auto constexpr desired_timeslices = 4; - Manifold_3 const manifold(desired_simplices, desired_timeslices); + Manifold_3 const manifold(desired_simplices, desired_timeslices, + cdt::Random{92}); THEN("Triangulation is valid.") { REQUIRE(manifold.is_correct()); } THEN("The geometry matches the triangulation.") { @@ -413,16 +390,14 @@ SCENARIO("3-Manifold initialization" * doctest::test_suite("manifold")) REQUIRE_EQ(manifold.edges(), manifold.N1()); REQUIRE_EQ(manifold.faces(), manifold.N2()); REQUIRE(manifold.check_simplices()); - // Human verification - manifold.print(); - manifold.print_volume_per_timeslice(); } } WHEN("Constructing a medium triangulation.") { auto constexpr desired_simplices = 6400; auto constexpr desired_timeslices = 7; - Manifold_3 const manifold(desired_simplices, desired_timeslices); + Manifold_3 const manifold(desired_simplices, desired_timeslices, + cdt::Random{92}); THEN("Triangulation is valid.") { REQUIRE(manifold.is_correct()); } THEN("The geometry matches the triangulation.") { @@ -431,9 +406,6 @@ SCENARIO("3-Manifold initialization" * doctest::test_suite("manifold")) REQUIRE_EQ(manifold.edges(), manifold.N1()); REQUIRE_EQ(manifold.faces(), manifold.N2()); REQUIRE(manifold.check_simplices()); - // Human verification - manifold.print(); - manifold.print_volume_per_timeslice(); } } } @@ -463,7 +435,8 @@ SCENARIO("3-Manifold function checks" * doctest::test_suite("manifold")) { auto constexpr desired_timeslices = 4; auto constexpr desired_simplices = 640; - Manifold_3 const manifold(desired_simplices, desired_timeslices); + Manifold_3 const manifold(desired_simplices, desired_timeslices, + cdt::Random{92}); THEN("Functions referencing geometry data are accurate") { CHECK_EQ(manifold.N3(), manifold.get_geometry().N3); @@ -487,7 +460,7 @@ SCENARIO("3-Manifold copying" * doctest::test_suite("manifold")) { auto constexpr desired_simplices = 640; auto constexpr desired_timeslices = 4; - Manifold_3 manifold(desired_simplices, desired_timeslices); + Manifold_3 manifold(desired_simplices, desired_timeslices, cdt::Random{92}); WHEN("It is copied.") { auto manifold2 = manifold; @@ -512,19 +485,6 @@ SCENARIO("3-Manifold copying" * doctest::test_suite("manifold")) CHECK_EQ(manifold2.N0(), manifold.N0()); CHECK_EQ(manifold2.max_time(), manifold.max_time()); CHECK_EQ(manifold2.min_time(), manifold.min_time()); - // Human verification - fmt::print("Manifold properties:\n"); - manifold.print(); - manifold.print_volume_per_timeslice(); - auto snapshot = manifold.delaunay_snapshot(); - auto cells = snapshot.tds().cells(); - fmt::print("Cell compact container size == {}\n", cells.size()); - fmt::print("Now compact container size == {}\n", cells.size()); - fmt::print("Vertex compact container size == {}\n", - snapshot.tds().vertices().size()); - fmt::print("Copied manifold properties:\n"); - manifold2.print(); - manifold2.print_volume_per_timeslice(); } } } @@ -536,7 +496,7 @@ SCENARIO("3-Manifold moving" * doctest::test_suite("manifold")) { auto constexpr desired_simplices = 64; auto constexpr desired_timeslices = 4; - Manifold_3 source(desired_simplices, desired_timeslices); + Manifold_3 source(desired_simplices, desired_timeslices, cdt::Random{92}); auto const expected_simplices = source.simplices(); auto const expected_faces = source.faces(); auto const expected_edges = source.edges(); @@ -561,7 +521,8 @@ SCENARIO("3-Manifold moving" * doctest::test_suite("manifold")) } WHEN("It is move assigned over another manifold.") { - Manifold_3 assigned(desired_simplices * 2, desired_timeslices); + Manifold_3 assigned(desired_simplices * 2, desired_timeslices, + cdt::Random{93}); auto const replaced_simplices = assigned.simplices(); assigned = std::move(source); @@ -579,7 +540,8 @@ SCENARIO("3-Manifold moving" * doctest::test_suite("manifold")) CHECK(source.is_correct()); CHECK_EQ(source.simplices(), replaced_simplices); - source = Manifold_3{desired_simplices, desired_timeslices}; + source = + Manifold_3{desired_simplices, desired_timeslices, cdt::Random{92}}; CHECK(source.is_correct()); } } @@ -593,19 +555,15 @@ SCENARIO("3-Manifold value rebuild" * doctest::test_suite("manifold")) { auto constexpr desired_simplices = 640; auto constexpr desired_timeslices = 4; - Manifold_3 manifold(desired_simplices, desired_timeslices); + Manifold_3 manifold(desired_simplices, desired_timeslices, cdt::Random{92}); WHEN("We rebuild it as a new value.") { // Get values for manifold1 - auto manifold_N3 = manifold.N3(); - auto manifold_N2 = manifold.N2(); - auto manifold_N1 = manifold.N1(); - auto manifold_N0 = manifold.N0(); - fmt::print("Manifold N3 = {}\n", manifold_N3); - fmt::print("Manifold N2 = {}\n", manifold_N2); - fmt::print("Manifold N1 = {}\n", manifold_N1); - fmt::print("Manifold N0 = {}\n", manifold_N0); - auto const rebuilt = manifold.updated(); + auto manifold_N3 = manifold.N3(); + auto manifold_N2 = manifold.N2(); + auto manifold_N1 = manifold.N1(); + auto manifold_N0 = manifold.N0(); + auto const rebuilt = manifold.updated(); THEN("The rebuilt value and source have the same geometry.") { CHECK_EQ(rebuilt.N3(), manifold_N3); @@ -628,29 +586,23 @@ SCENARIO("3-Manifold mutation" * doctest::test_suite("manifold")) { auto constexpr desired_simplices = 640; auto constexpr desired_timeslices = 4; - Manifold_3 manifold1(desired_simplices, desired_timeslices); - Manifold_3 const manifold2(desired_simplices, desired_timeslices); + Manifold_3 manifold1(desired_simplices, desired_timeslices, + cdt::Random{92}); + Manifold_3 const manifold2(desired_simplices, desired_timeslices, + cdt::Random{93}); WHEN("We construct a replacement value from the second triangulation.") { // Get values for manifold1 - auto manifold1_N3 = manifold1.N3(); - auto manifold1_N2 = manifold1.N2(); - auto manifold1_N1 = manifold1.N1(); - auto manifold1_N0 = manifold1.N0(); - fmt::print("Manifold 1 N3 = {}\n", manifold1_N3); - fmt::print("Manifold 1 N2 = {}\n", manifold1_N2); - fmt::print("Manifold 1 N1 = {}\n", manifold1_N1); - fmt::print("Manifold 1 N0 = {}\n", manifold1_N0); + auto manifold1_N3 = manifold1.N3(); + auto manifold1_N2 = manifold1.N2(); + auto manifold1_N1 = manifold1.N1(); + auto manifold1_N0 = manifold1.N0(); // Get values for manifold2 - auto manifold2_N3 = manifold2.N3(); - auto manifold2_N2 = manifold2.N2(); - auto manifold2_N1 = manifold2.N1(); - auto manifold2_N0 = manifold2.N0(); - fmt::print("Manifold 2 N3 = {}\n", manifold2_N3); - fmt::print("Manifold 2 N2 = {}\n", manifold2_N2); - fmt::print("Manifold 2 N1 = {}\n", manifold2_N1); - fmt::print("Manifold 2 N0 = {}\n", manifold2_N0); - auto const replacement = Manifold_3{ + auto manifold2_N3 = manifold2.N3(); + auto manifold2_N2 = manifold2.N2(); + auto manifold2_N1 = manifold2.N1(); + auto manifold2_N0 = manifold2.N0(); + auto const replacement = Manifold_3{ foliated_triangulations::FoliatedTriangulation_3{ manifold2.delaunay_snapshot(), manifold2.initial_radius(), manifold2.foliation_spacing()} @@ -695,15 +647,9 @@ SCENARIO("3-Manifold validation and fixing" * doctest::test_suite("manifold")) REQUIRE_EQ(manifold.max_time(), 3); } THEN("Every vertex in the manifold has a correct timevalue.") - { - manifold.print_vertices(); - REQUIRE(manifold.check_vertices()); - } + { REQUIRE(manifold.check_vertices()); } THEN("Every cell in the manifold is correctly classified.") - { - manifold.print_cells(); - REQUIRE(manifold.check_simplices()); - } + { REQUIRE(manifold.check_simplices()); } } WHEN("We insert an invalid timevalue into an owning snapshot.") { @@ -711,9 +657,7 @@ SCENARIO("3-Manifold validation and fixing" * doctest::test_suite("manifold")) auto cells = foliated_triangulations::collect_cells<3>(candidate); auto broken_cell = cells[0]; auto broken_vertex = broken_cell->vertex(0); - fmt::print("Info on vertex was {}\n", broken_vertex->info()); broken_vertex->info() = std::numeric_limits::max(); - fmt::print("Info on vertex is now {}\n", broken_vertex->info()); THEN("The snapshot is invalid while the source remains correct.") { CHECK(manifold.is_correct()); @@ -742,7 +686,8 @@ SCENARIO("3-Manifold validation and fixing" * doctest::test_suite("manifold")) { auto constexpr desired_timeslices = 7; auto constexpr desired_simplices = 6400; - Manifold_3 const manifold(desired_simplices, desired_timeslices); + Manifold_3 const manifold(desired_simplices, desired_timeslices, + cdt::Random{92}); THEN("The triangulation is valid and Delaunay.") { REQUIRE(manifold.is_correct()); } THEN("The geometry matches the triangulation.") diff --git a/tests/Metropolis_test.cpp b/tests/Metropolis_test.cpp index 003977d442..cb125570da 100644 --- a/tests/Metropolis_test.cpp +++ b/tests/Metropolis_test.cpp @@ -12,12 +12,122 @@ #include +#include +#include #include +#include +#include +#include #include +#include using namespace std; using namespace manifolds; +namespace +{ + [[nodiscard]] auto minimal_23_manifold() -> Manifold_3 + { + auto constexpr radius = 2.0 * std::numbers::inv_sqrt3_v; + auto constexpr root_2 = std::numbers::sqrt2_v; + vector vertices{ + Point_t<3>{ 1, 0, 0}, + Point_t<3>{ 0, 1, 0}, + Point_t<3>{ 0, 0, 1}, + Point_t<3>{radius, radius, radius}, + Point_t<3>{root_2, root_2, 0} + }; + vector timevalues{1, 1, 1, 2, 2}; + return Manifold_3{make_causal_vertices<3>(vertices, timevalues)}; + } + + [[nodiscard]] auto minimal_26_manifold() -> Manifold_3 + { + auto constexpr radius = 2.0 * std::numbers::inv_sqrt3_v; + vector vertices{ + Point_t<3>{ 0, 0, 0}, + Point_t<3>{ 1, 0, 0}, + Point_t<3>{ 0, 1, 0}, + Point_t<3>{ 0, 0, 1}, + Point_t<3>{radius, radius, radius} + }; + vector timevalues{0, 1, 1, 1, 2}; + return Manifold_3{make_causal_vertices<3>(vertices, timevalues)}; + } + + [[nodiscard]] auto actual_raw_site_count(Manifold_3 const& manifold, + move_tracker::move_type const move) + -> Int_precision + { + auto triangulation = manifold.delaunay_snapshot(); + auto const count = [](auto const& sites) { + return static_cast(sites.size()); + }; + + using enum move_tracker::move_type; + switch (move) + { + case TWO_THREE: + return count(foliated_triangulations::filter_cells<3>( + foliated_triangulations::collect_cells<3>(triangulation), + Cell_type::TWO_TWO)); + case THREE_TWO: + return count(foliated_triangulations::filter_edges<3>( + foliated_triangulations::collect_edges<3>(triangulation), true)); + case TWO_SIX: + return count(foliated_triangulations::filter_cells<3>( + foliated_triangulations::collect_cells<3>(triangulation), + Cell_type::ONE_THREE)); + case SIX_TWO: + return count( + foliated_triangulations::collect_vertices<3>(triangulation)); + case FOUR_FOUR: + return count(foliated_triangulations::filter_edges<3>( + foliated_triangulations::collect_edges<3>(triangulation), false)); + } + return 0; + } + + void check_proposal_domain(Manifold_3 const& manifold, + move_tracker::move_type const move) + { + auto const actual = actual_raw_site_count(manifold, move); + REQUIRE_GT(actual, 0); + CHECK_EQ(Metropolis_3::proposal_site_count(manifold.get_geometry(), move), + actual); + + auto const expected = 1.0L / (5.0L * static_cast(actual)); + auto const observed = + Metropolis_3::proposal_probability(manifold.get_geometry(), move); + CHECK(mpfr_values::to_long_double(observed) == doctest::Approx(expected)); + } + + class CountingGenerator + { + std::mt19937_64 m_engine; + std::size_t m_calls{0}; + + public: + using result_type = std::mt19937_64::result_type; + + explicit CountingGenerator(std::uint64_t const seed) : m_engine{seed} {} + + [[nodiscard]] static auto constexpr min() noexcept -> result_type + { return std::mt19937_64::min(); } + + [[nodiscard]] static auto constexpr max() noexcept -> result_type + { return std::mt19937_64::max(); } + + auto operator()() -> result_type + { + ++m_calls; + return m_engine(); + } + + [[nodiscard]] auto calls() const noexcept -> std::size_t { return m_calls; } + }; +} // namespace + static_assert(std::is_nothrow_swappable_v); SCENARIO("MoveStrategy special member and swap properties" * @@ -34,10 +144,10 @@ SCENARIO("MoveStrategy special member and swap properties" * REQUIRE(is_nothrow_destructible_v); spdlog::debug("It is no-throw destructible.\n"); } - THEN("It is no-throw default constructible.") + THEN("It is default constructible.") { - REQUIRE(is_nothrow_default_constructible_v); - spdlog::debug("It is no-throw default constructible.\n"); + REQUIRE(is_default_constructible_v); + spdlog::debug("It is default constructible.\n"); } THEN("It is no-throw copy constructible.") { @@ -71,6 +181,9 @@ SCENARIO("MoveStrategy special member and swap properties" * REQUIRE(is_constructible_v); + REQUIRE(is_constructible_v); spdlog::debug("Its file-output policy is configurable.\n"); } } @@ -88,12 +201,13 @@ SCENARIO("Metropolis member functions" * doctest::test_suite("metropolis")) auto constexpr timeslices = 4; auto constexpr output_every_n_passes = 1; auto constexpr passes = 10; - Manifold_3 const universe(simplices, timeslices); + Manifold_3 const universe(simplices, timeslices, cdt::Random{92}); // It is correctly constructed REQUIRE(universe.is_correct()); WHEN("A Metropolis function object is constructed.") { - Metropolis_3 testrun(Alpha, K, Lambda, passes, output_every_n_passes); + Metropolis_3 testrun(Alpha, K, Lambda, passes, output_every_n_passes, + true, 92); THEN("The Metropolis function object is initialized correctly.") { CHECK_EQ(testrun.Alpha(), Alpha); @@ -112,46 +226,22 @@ SCENARIO("Metropolis member functions" * doctest::test_suite("metropolis")) THEN("File output can be disabled without changing checkpoint cadence.") { Metropolis_3 const no_file_output_run(Alpha, K, Lambda, passes, - output_every_n_passes, false); + output_every_n_passes, false, 92); CHECK_EQ(no_file_output_run.checkpoint(), output_every_n_passes); CHECK_FALSE(no_file_output_run.writes_files()); } - THEN("The initial moves are made correctly.") + THEN("Initialization reads the canonical geometry without making moves.") { - auto result = testrun.initialize(universe); - auto total_rejected = testrun.get_rejected().total(); - auto total_attempted = testrun.get_attempted().total(); - auto total_successful = testrun.get_succeeded().total(); - auto total_failed = testrun.get_failed().total(); - // Initialization proposes one move of each type - for (auto i = 0; i < move_tracker::NUMBER_OF_3D_MOVES; ++i) - { - CHECK_EQ(testrun.get_proposed()[i], 1); - } - // Initialization accepts one move of each type - for (auto i = 0; i < move_tracker::NUMBER_OF_3D_MOVES; ++i) - { - CHECK_EQ(testrun.get_accepted()[i], 1); - } - // Initialization does not reject any moves - CHECK_EQ(total_rejected, 0); - // Initialization attempts one move of each type - for (auto i = 0; i < move_tracker::NUMBER_OF_3D_MOVES; ++i) - { - CHECK_EQ(testrun.get_attempted()[i], 1); - } - CHECK_EQ(total_attempted, total_successful + total_failed); - - // Human verification - REQUIRE_MESSAGE(result, - "The Metropolis function object failed to " - "initialize the universe."); - if (result) - { - result->print_attempts(); - result->print_successful(); - result->print_errors(); - } + testrun.initialize(universe); + CHECK_EQ(testrun.get_geometry().N1_TL, universe.N1_TL()); + CHECK_EQ(testrun.get_geometry().N3_31_13, universe.N3_31_13()); + CHECK_EQ(testrun.get_geometry().N3_22, universe.N3_22()); + CHECK_EQ(testrun.get_proposed().total(), 0); + CHECK_EQ(testrun.get_accepted().total(), 0); + CHECK_EQ(testrun.get_rejected().total(), 0); + CHECK_EQ(testrun.get_attempted().total(), 0); + CHECK_EQ(testrun.get_succeeded().total(), 0); + CHECK_EQ(testrun.get_failed().total(), 0); } } WHEN("A nonpositive pass or checkpoint count is supplied.") @@ -159,12 +249,12 @@ SCENARIO("Metropolis member functions" * doctest::test_suite("metropolis")) THEN("Construction rejects the invalid cadence.") { CHECK_THROWS_AS( - Metropolis_3(Alpha, K, Lambda, -1, output_every_n_passes), + Metropolis_3(Alpha, K, Lambda, -1, output_every_n_passes, true, 92), std::invalid_argument); CHECK_THROWS_AS( - Metropolis_3(Alpha, K, Lambda, 0, output_every_n_passes), + Metropolis_3(Alpha, K, Lambda, 0, output_every_n_passes, true, 92), std::invalid_argument); - CHECK_THROWS_AS(Metropolis_3(Alpha, K, Lambda, passes, 0), + CHECK_THROWS_AS(Metropolis_3(Alpha, K, Lambda, passes, 0, true, 92), std::invalid_argument); } } @@ -172,18 +262,269 @@ SCENARIO("Metropolis member functions" * doctest::test_suite("metropolis")) { THEN("Construction reports the corresponding parameter error.") { - CHECK_THROWS_AS( - Metropolis_3(0.5L, K, Lambda, passes, output_every_n_passes), - std::domain_error); + CHECK_THROWS_AS(Metropolis_3(0.5L, K, Lambda, passes, + output_every_n_passes, true, 92), + std::domain_error); CHECK_THROWS_AS( Metropolis_3(std::numeric_limits::infinity(), K, - Lambda, passes, output_every_n_passes), + Lambda, passes, output_every_n_passes, true, 92), std::invalid_argument); } } } } +SCENARIO("Metropolis-Hastings proposal and acceptance ratios" * + doctest::test_suite("metropolis")) +{ + auto constexpr Alpha = 0.6L; + Metropolis_3 strategy(Alpha, 0.0L, 0.0L, 1, 1, false, 17); + + GIVEN("A small pair of states connected by a (2,3) move.") + { + Geometry_3 current; + current.N3_22 = 4; + Geometry_3 proposed; + proposed.N1_TL = 10; + + WHEN("The forward and inverse proposal ratios are evaluated.") + { + auto const forward = Metropolis_3::CalculateA1( + current, proposed, move_tracker::move_type::TWO_THREE); + auto const reverse = Metropolis_3::CalculateA1( + proposed, current, move_tracker::move_type::THREE_TWO); + auto const round_trip = mpfr_values::multiply(forward, reverse); + + THEN("They are the exact reverse-to-forward ratios.") + { + CHECK(mpfr_values::to_long_double(forward) == doctest::Approx(0.4L)); + CHECK(mpfr_values::to_long_double(reverse) == doctest::Approx(2.5L)); + CHECK(mpfr_values::to_long_double(round_trip) == doctest::Approx(1.0L)); + } + AND_THEN("The zero-action acceptance probability is the Hastings ratio.") + { + auto const probability = strategy.acceptance_probability( + current, proposed, move_tracker::move_type::TWO_THREE); + CHECK(mpfr_values::to_long_double(probability) == + doctest::Approx(0.4L)); + } + } + } + + GIVEN("A (2,6) move and its inverse.") + { + Geometry_3 current; + current.N3_13 = 3; + Geometry_3 proposed; + proposed.N0 = 8; + + THEN("The one-three-cell and vertex domains determine the ratio.") + { + auto const forward = Metropolis_3::CalculateA1( + current, proposed, move_tracker::move_type::TWO_SIX); + auto const reverse = Metropolis_3::CalculateA1( + proposed, current, move_tracker::move_type::SIX_TWO); + CHECK(mpfr_values::to_long_double(forward) == + doctest::Approx(3.0L / 8.0L)); + CHECK(mpfr_values::to_long_double(reverse) == + doctest::Approx(8.0L / 3.0L)); + } + } + + GIVEN("A (4,4) move with unchanged spacelike-edge count.") + { + Geometry_3 current; + Geometry_3 proposed; + current.N1_SL = 7; + proposed.N1_SL = 7; + THEN("The self-inverse proposal is symmetric.") + { + auto const ratio = Metropolis_3::CalculateA1( + current, proposed, move_tracker::move_type::FOUR_FOUR); + CHECK(mpfr_values::to_long_double(ratio) == doctest::Approx(1.0L)); + } + } +} + +SCENARIO("Metropolis proposal domains match the sampled raw sites" * + doctest::test_suite("metropolis")) +{ + GIVEN("Minimal triangulations with nonempty raw proposal domains.") + { + auto const two_three_state = minimal_23_manifold(); + auto const two_six_state = minimal_26_manifold(); + + THEN("Every declared site count matches an independent enumeration.") + { + CHECK_EQ(actual_raw_site_count(two_three_state, + move_tracker::move_type::TWO_THREE), + 1); + CHECK_EQ(actual_raw_site_count(two_three_state, + move_tracker::move_type::THREE_TWO), + 5); + CHECK_EQ(actual_raw_site_count(two_six_state, + move_tracker::move_type::TWO_SIX), + 1); + CHECK_EQ(actual_raw_site_count(two_six_state, + move_tracker::move_type::SIX_TWO), + 5); + CHECK_EQ(actual_raw_site_count(two_six_state, + move_tracker::move_type::FOUR_FOUR), + 3); + + check_proposal_domain(two_three_state, + move_tracker::move_type::TWO_THREE); + check_proposal_domain(two_three_state, + move_tracker::move_type::THREE_TWO); + check_proposal_domain(two_six_state, move_tracker::move_type::TWO_SIX); + check_proposal_domain(two_six_state, move_tracker::move_type::SIX_TWO); + check_proposal_domain(two_six_state, move_tracker::move_type::FOUR_FOUR); + } + } + + GIVEN("Five labeled raw sites and a deterministic random engine.") + { + std::array const sites{0, 1, 2, 3, 4}; + std::array selections{}; + std::mt19937_64 generator{92}; + auto constexpr draws = std::size_t{50'000}; + + WHEN("The production one-site selector is sampled repeatedly.") + { + for (std::size_t draw = 0; draw < draws; ++draw) + { + auto const selected = + ergodic_moves::detail::random_element(sites, generator); + REQUIRE(selected.has_value()); + ++selections.at(static_cast(*selected)); + } + + THEN("All sites remain within a conservative uniformity envelope.") + { + auto constexpr expected = draws / sites.size(); + auto constexpr tolerance = expected / 10; + for (auto const selected : selections) + { + CHECK_GE(selected, expected - tolerance); + CHECK_LE(selected, expected + tolerance); + } + } + } + } +} + +SCENARIO("The (6,2) proposal uses the caller-owned generator throughout" * + doctest::test_suite("metropolis")) +{ + GIVEN("A minimal triangulation containing one removable (2,6) vertex.") + { + cdt::Random setup_random{92}; + CAPTURE(setup_random.seed()); + auto expanded = + ergodic_moves::do_26_move(minimal_26_manifold(), setup_random); + REQUIRE(expanded.has_value()); + auto const state = std::move(expanded).value(); + auto const triangulation = state.delaunay_snapshot(); + auto const vertices = + foliated_triangulations::collect_vertices<3>(triangulation); + + WHEN("Several seeds select that vertex and construct its inverse move.") + { + std::optional> reference; + std::size_t successful_seeds{0}; + for (std::uint64_t seed = 0; seed < 512 && successful_seeds < 8; ++seed) + { + CountingGenerator selector{seed}; + auto const selected = + ergodic_moves::detail::random_element(vertices, selector); + if (!selected || + !ergodic_moves::is_62_movable(triangulation, *selected)) + { + continue; + } + + auto const selection_calls = selector.calls(); + CountingGenerator proposal_generator{seed}; + auto candidate = + ergodic_moves::propose_62_move(state, proposal_generator); + CAPTURE(seed); + REQUIRE(candidate.has_value()); + CHECK_GT(proposal_generator.calls(), selection_calls); + + auto snapshot = candidate->delaunay_snapshot(); + if (reference) { CHECK_EQ(snapshot, *reference); } + else + { + reference = std::move(snapshot); + } + ++successful_seeds; + } + THEN("The RNG path and proposed state are reproducible across seeds.") + { REQUIRE_EQ(successful_seeds, 8); } + } + } +} + +SCENARIO("Metropolis transitions are sequential and failure-aware" * + doctest::test_suite("metropolis")) +{ + auto constexpr Alpha = 0.6L; + GIVEN("The minimal manifold supporting a (2,6) move.") + { + auto manifold = minimal_26_manifold(); + REQUIRE(manifold.is_correct()); + Metropolis_3 strategy(Alpha, 0.0L, 0.0L, 1, 1, false, 23); + + WHEN("Two always-accepted candidates are executed sequentially.") + { + REQUIRE(strategy.attempt_transition( + manifold, move_tracker::move_type::TWO_SIX, 0.0L)); + auto const after_first = manifold.get_geometry(); + REQUIRE(strategy.attempt_transition( + manifold, move_tracker::move_type::TWO_SIX, 0.0L)); + + THEN("The second candidate starts from the first committed state.") + { + CHECK_EQ(after_first.N3_31_13, 6); + CHECK_EQ(manifold.N3_31_13(), 10); + CHECK_EQ(manifold.N3_22(), 0); + CHECK_EQ(strategy.get_geometry().N3_31_13, manifold.N3_31_13()); + CHECK_EQ(strategy.get_geometry().N3_22, manifold.N3_22()); + CHECK_EQ(strategy.get_proposed().total(), 2); + CHECK_EQ(strategy.get_accepted().total(), 2); + CHECK_EQ(strategy.get_rejected().total(), 0); + CHECK_EQ(strategy.get_attempted().total(), 2); + CHECK_EQ(strategy.get_succeeded().total(), 2); + CHECK_EQ(strategy.get_failed().total(), 0); + } + } + } + + GIVEN("A manifold on which no (6,2) site is movable.") + { + auto manifold = minimal_26_manifold(); + auto const before = manifold.delaunay_snapshot(); + Metropolis_3 strategy(Alpha, 0.0L, 0.0L, 1, 1, false, 29); + + WHEN("The impossible proposal is attempted.") + { + auto const accepted = strategy.attempt_transition( + manifold, move_tracker::move_type::SIX_TWO, 0.0L); + THEN("It is an explicit rejected self-transition.") + { + CHECK_FALSE(accepted); + CHECK_EQ(manifold.delaunay_snapshot(), before); + CHECK_EQ(strategy.get_proposed().total(), 1); + CHECK_EQ(strategy.get_accepted().total(), 0); + CHECK_EQ(strategy.get_rejected().total(), 1); + CHECK_EQ(strategy.get_attempted().total(), 1); + CHECK_EQ(strategy.get_succeeded().total(), 0); + CHECK_EQ(strategy.get_failed().total(), 1); + } + } + } +} + SCENARIO("Using the Metropolis algorithm" * doctest::test_suite("metropolis")) { auto constexpr Alpha = static_cast(0.6); @@ -193,14 +534,15 @@ SCENARIO("Using the Metropolis algorithm" * doctest::test_suite("metropolis")) { auto constexpr simplices = 640; auto constexpr timeslices = 4; - Manifold_3 const universe(simplices, timeslices); + Manifold_3 const universe(simplices, timeslices, cdt::Random{92}); // It is correctly constructed REQUIRE(universe.is_correct()); WHEN("A Metropolis function object is constructed.") { auto constexpr output_every_n_passes = 1; auto constexpr passes = 1; - Metropolis_3 testrun(Alpha, K, Lambda, passes, output_every_n_passes); + Metropolis_3 testrun(Alpha, K, Lambda, passes, output_every_n_passes, + false, 31); THEN("A lot of moves are done.") { auto result = testrun(universe); @@ -214,19 +556,60 @@ SCENARIO("Using the Metropolis algorithm" * doctest::test_suite("metropolis")) auto total_attempted = testrun.get_attempted().total(); auto total_successful = testrun.get_succeeded().total(); auto total_failed = testrun.get_failed().total(); - // We should have at least a trial move per simplex on average - // per pass, times the number of passes - CHECK_GT(total_proposed, universe.N3() * passes); + CHECK_EQ(total_proposed, universe.N3() * passes); CHECK_EQ(total_proposed, total_accepted + total_rejected); - // We should attempt a move for each accepted move - CHECK_EQ(total_attempted, total_accepted); + CHECK_EQ(total_attempted, total_proposed); CHECK_GT(total_successful, 0); CHECK_GE(total_failed, 0); CHECK_EQ(total_attempted, total_successful + total_failed); - // Human verification - testrun.print_results(); + CHECK_LE(total_accepted, total_successful); + CHECK_LE(total_failed, total_rejected); + CHECK_EQ(testrun.get_geometry().N3, result.N3()); + CHECK_EQ(testrun.get_geometry().N3_31_13, result.N3_31_13()); + CHECK_EQ(testrun.get_geometry().N3_22, result.N3_22()); } } } } } + +SCENARIO("Metropolis runs replay every transition from one seed" * + doctest::test_suite("metropolis")) +{ + auto const initial = minimal_23_manifold(); + auto constexpr seed = cdt::Random_seed{92}; + auto constexpr passes = Int_precision{2}; + CAPTURE(seed); + Metropolis_3 first{ + 0.6L, + 0.0L, + 0.0L, + passes, + passes, + false, + cdt::Random{seed, cdt::random_streams::transitions} + }; + Metropolis_3 replay{ + 0.6L, + 0.0L, + 0.0L, + passes, + passes, + false, + cdt::Random{seed, cdt::random_streams::transitions} + }; + + auto const first_result = first(initial); + auto const replay_result = replay(initial); + auto const same_counts = [](auto const& lhs, auto const& rhs) { + return std::ranges::equal(lhs.moves_view(), rhs.moves_view()); + }; + + CHECK_EQ(first_result.delaunay_snapshot(), replay_result.delaunay_snapshot()); + CHECK(same_counts(first.get_proposed(), replay.get_proposed())); + CHECK(same_counts(first.get_accepted(), replay.get_accepted())); + CHECK(same_counts(first.get_rejected(), replay.get_rejected())); + CHECK(same_counts(first.get_attempted(), replay.get_attempted())); + CHECK(same_counts(first.get_succeeded(), replay.get_succeeded())); + CHECK(same_counts(first.get_failed(), replay.get_failed())); +} diff --git a/tests/Move_always_test.cpp b/tests/Move_always_test.cpp index 85fb27041c..fd699ecfbf 100644 --- a/tests/Move_always_test.cpp +++ b/tests/Move_always_test.cpp @@ -33,10 +33,10 @@ SCENARIO("MoveStrategy special member and swap properties" * REQUIRE(is_nothrow_destructible_v); spdlog::debug("It is no-throw destructible.\n"); } - THEN("It is no-throw default constructible.") + THEN("It is default constructible.") { - REQUIRE(is_nothrow_default_constructible_v); - spdlog::debug("It is no-throw default constructible.\n"); + REQUIRE(is_default_constructible_v); + spdlog::debug("It is default constructible.\n"); } THEN("It is no-throw copy constructible.") { @@ -79,20 +79,22 @@ SCENARIO("MoveAlways member functions" * doctest::test_suite("move_always")) { auto constexpr simplices = 640; auto constexpr timeslices = 4; - Manifold_3 const manifold(simplices, timeslices); + Manifold_3 const manifold(simplices, timeslices, cdt::Random{92}); REQUIRE(manifold.is_correct()); WHEN("A MoveAlways_3 is constructed.") { auto constexpr passes = 10; auto constexpr checkpoint = 5; - MoveAlways_3 const mover(passes, checkpoint); + MoveAlways_3 const mover(passes, checkpoint, cdt::Random_seed{92}); THEN("The correct passes and checkpoints are instantiated.") { CHECK_EQ(mover.passes(), passes); CHECK_EQ(mover.checkpoint(), checkpoint); } - CHECK_THROWS_AS(MoveAlways_3(-1, checkpoint), std::invalid_argument); - CHECK_THROWS_AS(MoveAlways_3(passes, 0), std::invalid_argument); + CHECK_THROWS_AS(MoveAlways_3(-1, checkpoint, cdt::Random_seed{92}), + std::invalid_argument); + CHECK_THROWS_AS(MoveAlways_3(passes, 0, cdt::Random_seed{92}), + std::invalid_argument); THEN("Attempted, successful, and failed moves are zero-initialized.") { CHECK_EQ(mover.get_attempted().total(), 0); @@ -104,7 +106,7 @@ SCENARIO("MoveAlways member functions" * doctest::test_suite("move_always")) { auto constexpr passes = 1; auto constexpr checkpoint = 1; - MoveAlways_3 const mover(passes, checkpoint); + MoveAlways_3 const mover(passes, checkpoint, cdt::Random_seed{92}); THEN("The correct passes and checkpoints are instantiated.") { CHECK_EQ(mover.passes(), passes); @@ -120,23 +122,20 @@ SCENARIO("MoveAlways member functions" * doctest::test_suite("move_always")) } } -// This may take a while, so the scenario decorated with doctest::skip() -// to disable by default -SCENARIO("Using the MoveAlways algorithm" * doctest::test_suite("move_always") * - doctest::skip()) +SCENARIO("Using the MoveAlways algorithm" * doctest::test_suite("move_always")) { spdlog::debug("Using the MoveAlways algorithm.\n"); GIVEN("A correctly-constructed Manifold_3.") { auto constexpr simplices = 64; auto constexpr timeslices = 3; - Manifold_3 const manifold(simplices, timeslices); + Manifold_3 const manifold(simplices, timeslices, cdt::Random{92}); REQUIRE(manifold.is_correct()); WHEN("A MoveAlways_3 algorithm is used.") { auto constexpr passes = 1; - auto constexpr checkpoint = 1; - MoveAlways_3 mover(passes, checkpoint); + auto constexpr checkpoint = 2; + MoveAlways_3 mover(passes, checkpoint, cdt::Random_seed{92}); THEN("A lot of moves are made.") { auto result = mover(manifold); @@ -146,10 +145,9 @@ SCENARIO("Using the MoveAlways algorithm" * doctest::test_suite("move_always") * "The correct number of attempted, successful, and failed moves are " "made.") { + CHECK_EQ(mover.get_attempted().total(), manifold.N3()); CHECK_EQ(mover.get_attempted().total(), mover.get_succeeded().total() + mover.get_failed().total()); - // Human verification - mover.print_results(); } } } diff --git a/tests/Move_command_test.cpp b/tests/Move_command_test.cpp index b45afa28b0..f504cf2824 100644 --- a/tests/Move_command_test.cpp +++ b/tests/Move_command_test.cpp @@ -11,7 +11,6 @@ #include "Move_command.hpp" #include -#include #include #include @@ -21,6 +20,8 @@ #include #include +#include "Apply_move.hpp" + using namespace std; using namespace manifolds; @@ -183,21 +184,18 @@ SCENARIO("Invoking a move with a function pointer" * { auto constexpr desired_simplices = 640; auto constexpr desired_timeslices = 4; - Manifold_3 manifold(desired_simplices, desired_timeslices); + Manifold_3 manifold(desired_simplices, desired_timeslices, cdt::Random{92}); REQUIRE(manifold.is_correct()); WHEN("A function pointer is constructed for a move.") { - auto* const move23{ergodic_moves::do_23_move}; + auto* const move23{ergodic_moves::do_23_move}; THEN("Running the function makes the move.") { - auto result = move23(manifold); + cdt::Random random{92}; + CAPTURE(random.seed()); + auto result = move23(manifold, random); CHECK(ergodic_moves::check_move(manifold, result.value(), move_tracker::move_type::TWO_THREE)); - // Human verification - fmt::print("Manifold properties:\n"); - manifold.print_details(); - fmt::print("Moved manifold properties:\n"); - result->print_details(); } } } @@ -210,23 +208,21 @@ SCENARIO("Invoking a move with a lambda" * doctest::test_suite("move_command")) { auto constexpr desired_simplices = 640; auto constexpr desired_timeslices = 4; - Manifold_3 manifold(desired_simplices, desired_timeslices); + Manifold_3 manifold(desired_simplices, desired_timeslices, cdt::Random{92}); REQUIRE(manifold.is_correct()); WHEN("A lambda is constructed for a move.") { - auto const move23 = [](Manifold_3 const& manifold_3) { - return ergodic_moves::do_23_move(manifold_3).value(); + auto const move23 = [](Manifold_3 const& manifold_3, + cdt::Random& random) { + return ergodic_moves::do_23_move(manifold_3, random).value(); }; THEN("Running the lambda makes the move.") { - auto result = move23(manifold); + cdt::Random random{92}; + CAPTURE(random.seed()); + auto result = move23(manifold, random); CHECK(ergodic_moves::check_move(manifold, result, move_tracker::move_type::TWO_THREE)); - // Human verification - fmt::print("Manifold properties:\n"); - manifold.print_details(); - fmt::print("Moved manifold properties:\n"); - result.print_details(); } } } @@ -240,21 +236,18 @@ SCENARIO("Invoking a move with apply_move and a function pointer" * { auto constexpr desired_simplices = 640; auto constexpr desired_timeslices = 4; - Manifold_3 manifold(desired_simplices, desired_timeslices); + Manifold_3 manifold(desired_simplices, desired_timeslices, cdt::Random{92}); REQUIRE(manifold.is_correct()); WHEN("Apply_move is used for a move.") { - auto* move = ergodic_moves::do_23_move; + auto* move = ergodic_moves::do_23_move; THEN("Invoking apply_move() makes the move.") { - auto result = apply_move(manifold, move); + cdt::Random random{92}; + CAPTURE(random.seed()); + auto result = apply_move(manifold, move, random); CHECK(ergodic_moves::check_move(manifold, result.value(), move_tracker::move_type::TWO_THREE)); - // Human verification - fmt::print("Manifold properties:\n"); - manifold.print_details(); - fmt::print("Moved manifold properties:\n"); - result->print_details(); } } } @@ -267,17 +260,12 @@ SCENARIO("MoveCommand initialization" * doctest::test_suite("move_command")) { auto constexpr desired_simplices = 640; auto constexpr desired_timeslices = 4; - Manifold_3 manifold(desired_simplices, desired_timeslices); + Manifold_3 manifold(desired_simplices, desired_timeslices, cdt::Random{92}); REQUIRE(manifold.is_correct()); WHEN("A Command is constructed with a manifold.") { MoveCommand const command(manifold); - THEN("The original is still valid.") - { - REQUIRE(manifold.is_correct()); - // Human verification - manifold.print_details(); - } + THEN("The original is still valid.") { REQUIRE(manifold.is_correct()); } THEN("It contains the manifold.") { CHECK_EQ(manifold.N3(), command.get_const_results().N3()); @@ -292,13 +280,6 @@ SCENARIO("MoveCommand initialization" * doctest::test_suite("move_command")) CHECK_EQ(manifold.N0(), command.get_const_results().N0()); CHECK_EQ(manifold.max_time(), command.get_const_results().max_time()); CHECK_EQ(manifold.min_time(), command.get_const_results().min_time()); - // Human verification - fmt::print("Manifold properties:\n"); - manifold.print_details(); - manifold.print_volume_per_timeslice(); - fmt::print("Command.get_const_results() properties:\n"); - command.get_const_results().print_details(); - command.get_const_results().print_volume_per_timeslice(); } THEN("The two manifolds are distinct.") { @@ -311,13 +292,6 @@ SCENARIO("MoveCommand initialization" * doctest::test_suite("move_command")) CHECK_EQ(command.get_attempted().total(), 0); CHECK_EQ(command.get_succeeded().total(), 0); CHECK_EQ(command.get_failed().total(), 0); - - // Human verification - fmt::print("Attempted moves are {}\n", - command.get_attempted().moves_view()); - fmt::print("Successful moves are {}\n", - command.get_succeeded().moves_view()); - fmt::print("Failed moves are {}\n", command.get_failed().moves_view()); } } } @@ -330,7 +304,7 @@ SCENARIO("Queueing and executing moves" * doctest::test_suite("move_command")) { auto constexpr desired_simplices = 9600; auto constexpr desired_timeslices = 7; - Manifold_3 manifold(desired_simplices, desired_timeslices); + Manifold_3 manifold(desired_simplices, desired_timeslices, cdt::Random{92}); REQUIRE(manifold.is_correct()); WHEN("Move_command copies the manifold and applies the move.") { @@ -343,7 +317,9 @@ SCENARIO("Queueing and executing moves" * doctest::test_suite("move_command")) command.enqueue(move_tracker::move_type::THREE_TWO); // Execute the move - command.execute(); + cdt::Random random{92}; + CAPTURE(random.seed()); + command.execute(random); check_single_move_outcome(command, manifold, move_tracker::move_type::THREE_TWO, -1); @@ -354,12 +330,8 @@ SCENARIO("Queueing and executing moves" * doctest::test_suite("move_command")) auto* manifold_ptr = &manifold; auto* result_ptr = &result; REQUIRE_FALSE(manifold_ptr == result_ptr); - fmt::print( - "The manifold and the result in the MoveCommand are distinct " - "pointers.\n"); CHECK(manifold.is_correct()); - fmt::print("The original manifold is unchanged by MoveCommand.\n"); } } WHEN("A (4,4) move is queued.") @@ -369,7 +341,9 @@ SCENARIO("Queueing and executing moves" * doctest::test_suite("move_command")) THEN("It is executed correctly.") { // Execute the move - command.execute(); + cdt::Random random{92}; + CAPTURE(random.seed()); + command.execute(random); check_single_move_outcome(command, manifold, move_tracker::move_type::FOUR_FOUR, 0); } @@ -381,7 +355,9 @@ SCENARIO("Queueing and executing moves" * doctest::test_suite("move_command")) THEN("It is executed correctly.") { // Execute the move - command.execute(); + cdt::Random random{92}; + CAPTURE(random.seed()); + command.execute(random); check_single_move_outcome(command, manifold, move_tracker::move_type::TWO_THREE, 1); } @@ -393,7 +369,9 @@ SCENARIO("Queueing and executing moves" * doctest::test_suite("move_command")) THEN("It is executed correctly.") { // Execute the move - command.execute(); + cdt::Random random{92}; + CAPTURE(random.seed()); + command.execute(random); check_single_move_outcome(command, manifold, move_tracker::move_type::THREE_TWO, -1); } @@ -405,7 +383,9 @@ SCENARIO("Queueing and executing moves" * doctest::test_suite("move_command")) THEN("It is executed correctly.") { // Execute the move - command.execute(); + cdt::Random random{92}; + CAPTURE(random.seed()); + command.execute(random); check_single_move_outcome(command, manifold, move_tracker::move_type::TWO_SIX, 4); } @@ -417,7 +397,9 @@ SCENARIO("Queueing and executing moves" * doctest::test_suite("move_command")) THEN("It is executed correctly.") { // Execute the move - command.execute(); + cdt::Random random{92}; + CAPTURE(random.seed()); + command.execute(random); check_single_move_outcome(command, manifold, move_tracker::move_type::SIX_TWO, -4); } @@ -444,7 +426,9 @@ SCENARIO("Rejected moves preserve manifold state" * WHEN("The move is executed.") { - command.execute(); + cdt::Random random{92}; + CAPTURE(random.seed()); + command.execute(random); THEN("The rejection leaves the complete manifold unchanged.") { REQUIRE_EQ(command.get_failed().two_three_moves(), 1); @@ -463,7 +447,8 @@ SCENARIO("Executing multiple moves on the queue" * { auto constexpr desired_simplices = 9600; auto constexpr desired_timeslices = 7; - Manifold_3 const manifold(desired_simplices, desired_timeslices); + Manifold_3 const manifold(desired_simplices, desired_timeslices, + cdt::Random{92}); REQUIRE(manifold.is_correct()); WHEN("(2,3) and (3,2) moves are queued.") { @@ -474,23 +459,18 @@ SCENARIO("Executing multiple moves on the queue" * THEN("The moves are executed correctly.") { // Execute the moves - command.execute(); + cdt::Random random{92}; + CAPTURE(random.seed()); + command.execute(random); // There should be 2 attempted moves CHECK_EQ(command.get_attempted().total(), 2); - command.print_attempts(); auto successful_23_moves = command.get_succeeded().two_three_moves(); - fmt::print("There was {} successful (2,3) move.\n", - successful_23_moves); - auto successful_32_moves = command.get_succeeded().three_two_moves(); - fmt::print("There was {} successful (3,2) move.\n", - successful_32_moves); CHECK_EQ(command.get_succeeded().total() + command.get_failed().total(), 2); - command.print_errors(); // Get the results auto const& result = command.get_const_results(); @@ -514,35 +494,20 @@ SCENARIO("Executing multiple moves on the queue" * THEN("The moves are executed correctly.") { // Execute the moves - command.execute(); + cdt::Random random{92}; + CAPTURE(random.seed()); + command.execute(random); // There should be 5 attempted moves CHECK_EQ(command.get_attempted().total(), 5); - command.print_attempts(); auto successful_23_moves = command.get_succeeded().two_three_moves(); - fmt::print("There was {} successful (2,3) move.\n", - successful_23_moves); - auto successful_26_moves = command.get_succeeded().two_six_moves(); - fmt::print("There was {} successful (2,6) move.\n", - successful_26_moves); - - auto successful_44_moves = command.get_succeeded().four_four_moves(); - fmt::print("There was {} successful (4,4) move.\n", - successful_44_moves); - auto successful_62_moves = command.get_succeeded().six_two_moves(); - fmt::print("There was {} successful (6,2) move.\n", - successful_62_moves); - auto successful_32_moves = command.get_succeeded().three_two_moves(); - fmt::print("There was {} successful (3,2) move.\n", - successful_32_moves); CHECK_EQ(command.get_succeeded().total() + command.get_failed().total(), 5); - command.print_errors(); // Get the results auto const& result = command.get_const_results(); diff --git a/tests/Random_benchmark.cpp b/tests/Random_benchmark.cpp new file mode 100644 index 0000000000..ecc7a23fdc --- /dev/null +++ b/tests/Random_benchmark.cpp @@ -0,0 +1,82 @@ +/******************************************************************************* + Causal Dynamical Triangulations in C++ using CGAL + + Copyright © 2026 Adam Getchell + ******************************************************************************/ + +/// @file Random_benchmark.cpp +/// @brief Before/after benchmark for move-heavy random selection + +#include +#include +#include +#include +#include +#include +#include + +#include "Move_tracker.hpp" + +namespace +{ + using Clock = std::chrono::steady_clock; + + [[nodiscard]] auto parse_draws(char const* argument) -> std::size_t + { + std::size_t draws{}; + auto const text = std::string_view{argument}; + auto const [end, error] = + std::from_chars(text.data(), text.data() + text.size(), draws); + if (error != std::errc{} || end != text.data() + text.size() || draws == 0) + { + throw std::invalid_argument{"draw count must be a positive integer"}; + } + return draws; + } + + template + [[nodiscard]] auto measure(std::size_t const draws, Draw&& draw) + -> std::pair + { + std::uint64_t checksum{}; + auto const start = Clock::now(); + for (std::size_t sample = 0; sample < draws; ++sample) + { + checksum += static_cast(move_tracker::as_integer(draw())); + } + return {std::chrono::duration_cast(Clock::now() - + start), + checksum}; + } +} // namespace + +auto main(int const argc, char const* const argv[]) -> int +try +{ + auto const draws = argc == 2 ? parse_draws(argv[1]) : std::size_t{10'000}; + + cdt::Random run_random{92}; + auto const [owned_time, owned_checksum] = measure(draws, [&run_random] { + return move_tracker::generate_random_move_3(run_random); + }); + auto const [entropy_time, entropy_checksum] = measure(draws, [] { + cdt::Random per_draw_random; + return move_tracker::generate_random_move_3(per_draw_random); + }); + + auto const owned_ns = static_cast(owned_time.count()); + auto const speedup = + static_cast(entropy_time.count()) / owned_ns; + std::cout << "draws=" << draws << '\n' + << "before_entropy_per_draw_ns=" << entropy_time.count() << '\n' + << "after_run_owned_pcg_ns=" << owned_time.count() << '\n' + << "speedup=" << static_cast(speedup) << '\n' + << "checksums=" << entropy_checksum << ',' << owned_checksum + << '\n'; + return 0; +} +catch (std::exception const& error) +{ + std::cerr << "rng benchmark: " << error.what() << '\n'; + return 2; +} diff --git a/tests/Random_header_consumer.cpp b/tests/Random_header_consumer.cpp new file mode 100644 index 0000000000..9d5696e804 --- /dev/null +++ b/tests/Random_header_consumer.cpp @@ -0,0 +1,5 @@ +#include + +#include "Random.hpp" + +static_assert(std::uniform_random_bit_generator); diff --git a/tests/Random_test.cpp b/tests/Random_test.cpp new file mode 100644 index 0000000000..0fba451182 --- /dev/null +++ b/tests/Random_test.cpp @@ -0,0 +1,117 @@ +/******************************************************************************* + Causal Dynamical Triangulations in C++ using CGAL + + Copyright © 2026 Adam Getchell + ******************************************************************************/ + +/// @file Random_test.cpp +/// @brief Replay, stream-splitting, and distribution-boundary tests + +#include "Random.hpp" + +#include + +#include +#include +#include + +#include "Foliated_triangulation.hpp" +#include "Utilities.hpp" + +SCENARIO("PCG runs are reproducible and independently split" * + doctest::test_suite("random")) +{ + auto constexpr seed = cdt::Random_seed{92}; + CAPTURE(seed); + + GIVEN("Two engines with the same seed and stream") + { + cdt::Random first{seed, cdt::random_streams::transitions}; + cdt::Random replay{seed, cdt::random_streams::transitions}; + + THEN("their complete sampled sequences are identical") + { + for (auto sample = 0; sample < 256; ++sample) + { + CHECK_EQ(first(), replay()); + } + CHECK_EQ(first.seed(), seed); + CHECK_EQ(first.stream(), cdt::random_streams::transitions); + } + } + + GIVEN("Two named streams split from the same root seed") + { + cdt::Random root{seed}; + auto initialization = root.split(cdt::random_streams::initialization); + auto transitions = root.split(cdt::random_streams::transitions); + auto transitions_replay = root.split(cdt::random_streams::transitions); + + std::array initialization_samples{}; + std::array transition_samples{}; + std::array replay_samples{}; + std::ranges::generate(initialization_samples, + [&initialization] { return initialization(); }); + std::ranges::generate(transition_samples, + [&transitions] { return transitions(); }); + std::ranges::generate( + replay_samples, [&transitions_replay] { return transitions_replay(); }); + + THEN("each stream replays itself without duplicating the other stream") + { + CHECK(transition_samples == replay_samples); + CHECK(initialization_samples != transition_samples); + } + } +} + +SCENARIO("Random distributions respect their boundaries" * + doctest::test_suite("random")) +{ + cdt::Random generator{92}; + CAPTURE(generator.seed()); + + THEN("integer, real, probability, timeslice, and die samples stay in range") + { + CHECK_EQ(utilities::generate_random_int(generator, 7, 7), 7); + for (auto sample = 0; sample < 1'000; ++sample) + { + auto const integer = utilities::generate_random_int(generator, -12, 34); + CHECK_GE(integer, -12); + CHECK_LE(integer, 34); + + auto const real = utilities::generate_random_real(generator, -2.5L, 4.5L); + CHECK_GE(real, -2.5L); + CHECK_LT(real, 4.5L); + + auto const probability = utilities::generate_probability(generator); + CHECK_GE(probability, 0.0L); + CHECK_LT(probability, 1.0L); + + auto const timeslice = utilities::generate_random_timeslice(generator, 8); + CHECK_GE(timeslice, 1); + CHECK_LE(timeslice, 8); + + auto const roll = utilities::die_roll(generator); + CHECK_GE(roll, 1); + CHECK_LE(roll, 6); + } + } +} + +SCENARIO("Initialization point generation replays from its named stream" * + doctest::test_suite("random")) +{ + cdt::Random first_root{92}; + cdt::Random replay_root{92}; + CAPTURE(first_root.seed()); + auto first_random = first_root.split(cdt::random_streams::initialization); + auto replay_random = replay_root.split(cdt::random_streams::initialization); + + auto const first_vertices = foliated_triangulations::make_foliated_ball<3>( + 160, 3, 1.0, 1.0, first_random); + auto const replay_vertices = foliated_triangulations::make_foliated_ball<3>( + 160, 3, 1.0, 1.0, replay_random); + + REQUIRE_EQ(first_vertices, replay_vertices); +} diff --git a/tests/S3Action_test.cpp b/tests/S3Action_test.cpp index b12e20b50b..0a9586650e 100644 --- a/tests/S3Action_test.cpp +++ b/tests/S3Action_test.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include "Manifold.hpp" @@ -40,6 +41,7 @@ SCENARIO("MPFR calculations use scope-owned values" * THEN("The calculation uses the configured precision.") { CHECK_EQ(numerator.get_precision(), mpfr_values::precision); + CHECK_EQ(mpfr_values::rounding_mode, MPFR_RNDN); CHECK(mpfr_values::to_long_double(quotient) == doctest::Approx(0.5L)); } AND_THEN("The process-wide default precision is unchanged.") @@ -76,7 +78,7 @@ SCENARIO("Calculate the bulk action on S3 triangulations" * auto constexpr timeslices = 7; auto constexpr K = 1.1L; // NOLINT auto constexpr Lambda = 0.1L; - Manifold_3 const universe(simplices, timeslices); + Manifold_3 const universe(simplices, timeslices, cdt::Random{92}); // Verify triangulation CHECK_EQ(universe.N3(), universe.simplices()); CHECK_EQ(universe.N1(), universe.edges()); @@ -84,8 +86,6 @@ SCENARIO("Calculate the bulk action on S3 triangulations" * CHECK_EQ(universe.dimensionality(), 3); CHECK(universe.is_correct()); - universe.print_volume_per_timeslice(); - CHECK_EQ(universe.max_time(), timeslices); CHECK_EQ(universe.min_time(), 1); WHEN("The alpha=-1 Bulk Action is calculated.") @@ -141,14 +141,39 @@ SCENARIO("Calculate the bulk action on S3 triangulations" * spdlog::debug("S3_bulk_action() = {}\n", Bulk_action.to_double()); spdlog::debug("S3_bulk_action_alpha_one() = {}\n", Bulk_action_one.to_double()); - REQUIRE(utilities::Gmpzf_to_double(Bulk_action_one) == - doctest::Approx(utilities::Gmpzf_to_double(Bulk_action)) + REQUIRE(mpfr_values::to_double(Bulk_action_one) == + doctest::Approx(mpfr_values::to_double(Bulk_action)) .epsilon(TOLERANCE)); } } } } +SCENARIO("Bulk action precision survives the acceptance boundary" * + doctest::test_suite("s3action")) +{ + GIVEN("Two large alpha=1 geometries with a sub-double action delta.") + { + auto constexpr large_count = Int_precision{1'000'000'000}; + auto const lambda = + (2.0L * std::numbers::pi_v - 5.355L) / 0.204L; + auto const current = S3_bulk_action_alpha_one(large_count, large_count, + large_count, 1.0L, lambda); + auto const proposed = S3_bulk_action_alpha_one( + large_count + 1, large_count, large_count + 1, 1.0L, lambda); + auto const delta = mpfr_values::subtract(current, proposed); + + THEN("The 256-bit values and their difference remain distinguishable.") + { + CHECK_EQ(current.get_precision(), mpfr_values::precision); + CHECK_EQ(proposed.get_precision(), mpfr_values::precision); + CHECK(mpfr_zero_p(delta.fr()) == 0); + CHECK_EQ(mpfr_values::to_double(current), + mpfr_values::to_double(proposed)); + } + } +} + SCENARIO("Bulk action rejects invalid physical parameters" * doctest::test_suite("s3action")) { diff --git a/tests/Settings_test.cpp b/tests/Settings_test.cpp index 417ed7d9d3..a03b1ff46f 100644 --- a/tests/Settings_test.cpp +++ b/tests/Settings_test.cpp @@ -30,26 +30,14 @@ SCENARIO("Check settings" * doctest::test_suite("settings")) WHEN("MPFR precision is queried.") { auto precision = PRECISION; - THEN("The value is 256 bits.") - { - fmt::print("MPFR precision set to {}.\n", precision); - REQUIRE_EQ(precision, 256); - } + THEN("The value is 256 bits.") { REQUIRE_EQ(precision, 256); } } WHEN("Memory alignment is queried.") { auto constexpr align_64 = ALIGNMENT_64_BIT; - THEN("The value is 64 bits.") - { - fmt::print("Memory alignment is set to {}.\n", align_64); - REQUIRE_EQ(align_64, 64); - } + THEN("The value is 64 bits.") { REQUIRE_EQ(align_64, 64); } auto constexpr align_32 = ALIGNMENT_32_BIT; - THEN("The value is 32 bits.") - { - fmt::print("Memory alignment is set to {}.\n", align_32); - REQUIRE_EQ(align_32, 32); - } + THEN("The value is 32 bits.") { REQUIRE_EQ(align_32, 32); } } } } diff --git a/tests/Tetrahedron_test.cpp b/tests/Tetrahedron_test.cpp index 02725b30d7..48cff84364 100644 --- a/tests/Tetrahedron_test.cpp +++ b/tests/Tetrahedron_test.cpp @@ -94,60 +94,29 @@ SCENARIO("Find distances between points of the tetrahedron" * { REQUIRE(triangulation.is_initialized()); } THEN("The squared distances of vertices from origin are correct.") { - fmt::print("v_1 is {}\n", utilities::point_to_str(v_1)); - fmt::print("v_2 is {}\n", utilities::point_to_str(v_2)); - fmt::print("v_3 is {}\n", utilities::point_to_str(v_3)); - fmt::print("v_4 is {}\n", utilities::point_to_str(v_4)); - auto d_1 = r_2(origin, v_1); - fmt::print("The squared distance between v_1 and the origin is {}\n", - d_1); CHECK_EQ(d_1, doctest::Approx(1.0)); auto d_2 = r_2(origin, v_2); - fmt::print("The squared distance between v_2 and the origin is {}\n", - d_2); CHECK_EQ(d_2, doctest::Approx(1.0)); auto d_3 = r_2(origin, v_3); - fmt::print("The squared distance between v_3 and the origin is {}\n", - d_3); CHECK_EQ(d_3, doctest::Approx(1.0)); auto d_4 = r_2(origin, v_4); - fmt::print("The squared distance between v_4 and the origin is {}\n", - d_4); CHECK_EQ(d_4, doctest::Approx(4.0)); } THEN("The squared distance between radius=1 vertices are 2.") { auto d_1 = r_2(v_1, v_2); CHECK_EQ(d_1, doctest::Approx(2.0)); - fmt::print("The squared distance between v_1 and v_2 is {}\n", d_1); auto d_2 = r_2(v_1, v_3); CHECK_EQ(d_2, doctest::Approx(2.0)); - fmt::print("The squared distance between v_1 and v_3 is {}\n", d_2); auto d_3 = r_2(v_2, v_3); CHECK_EQ(d_3, doctest::Approx(2.0)); - fmt::print("The squared distance between v_2 and v_3 is {}\n", d_3); } THEN("All vertices have correct timevalues.") - { - CHECK(triangulation.check_all_vertices()); - // Human verification - auto print = [&triangulation](Vertex_handle_t<3> const& vertex) { - fmt::print( - "Vertex ({}) with timevalue of {} has a squared radius of {} and " - "a squared expected radius of {} with an expected timevalue of " - "{}.\n", - utilities::point_to_str(vertex->point()), vertex->info(), - squared_radius<3>(vertex), - std::pow(triangulation.expected_radius(vertex), 2), - triangulation.expected_timevalue(vertex)); - }; - auto snapshot = triangulation.delaunay_snapshot(); - ranges::for_each(collect_vertices<3>(snapshot), print); - } + { CHECK(triangulation.check_all_vertices()); } } } } @@ -198,8 +167,6 @@ SCENARIO("Construct a foliated tetrahedron in a foliated triangulation" * auto snapshot = triangulation.delaunay_snapshot(); auto cell = snapshot.finite_cells_begin(); CHECK_EQ(expected_cell_type<3>(cell), Cell_type::THREE_ONE); - // Human verification - triangulation.print_cells(); } THEN("There is one (3,1) simplex.") diff --git a/tests/Utilities_test.cpp b/tests/Utilities_test.cpp index ed1b5fdfc3..d58c156454 100644 --- a/tests/Utilities_test.cpp +++ b/tests/Utilities_test.cpp @@ -10,18 +10,72 @@ /// @details Tests for random, conversion, and datetime functions. #include -#include +#include +#include +#include +#include #include #include #include #include +#include +#include using namespace std; using namespace utilities; namespace { + class TemporaryDirectory + { + std::filesystem::path m_path; + + public: + TemporaryDirectory() + { + static std::atomic sequence{}; + auto const base = std::filesystem::temp_directory_path(); + + for (std::uint64_t attempt = 0; attempt < 100; ++attempt) + { + auto const timestamp = + std::chrono::steady_clock::now().time_since_epoch().count(); + auto const candidate = + base / fmt::format("cdt-plusplus-tests-{}-{}-{}", timestamp, + sequence.fetch_add(1), attempt); + std::error_code error; + if (std::filesystem::create_directory(candidate, error)) + { + m_path = candidate; + return; + } + if (error) + { + throw std::filesystem::filesystem_error{ + "Unable to create test directory", candidate, error}; + } + } + + throw std::runtime_error{"Unable to create a unique test directory"}; + } + + TemporaryDirectory(TemporaryDirectory const&) = delete; + TemporaryDirectory(TemporaryDirectory&&) = delete; + auto operator=(TemporaryDirectory const&) -> TemporaryDirectory& = delete; + auto operator=(TemporaryDirectory&&) -> TemporaryDirectory& = delete; + + ~TemporaryDirectory() + { + std::error_code error; + std::filesystem::remove_all(m_path, error); + } + + [[nodiscard]] auto file(std::string_view const name) const + -> std::filesystem::path + { return m_path / name; } + }; + struct SerializationFailure {}; @@ -62,16 +116,14 @@ SCENARIO("Various string/stream/time utilities" * auto constexpr this_topology = topology_type::SPHERICAL; WHEN("Operator<< is invoked.") { - stringstream const buffer; - std::streambuf* backup = cout.rdbuf(buffer.rdbuf()); - cout << this_topology; - cout.rdbuf(backup); + stringstream buffer; + buffer << this_topology; THEN("The output is correct.") { CHECK_EQ(buffer.str(), "spherical"); spdlog::debug("buffer.str() contents: {}.\n", buffer.str()); } - WHEN("fmt::print is invoked.") + WHEN("fmt::format is invoked.") { THEN("The output is correct.") { @@ -93,8 +145,6 @@ SCENARIO("Various string/stream/time utilities" * auto const expected_year = date::format( "%Y", std::chrono::floor(timestamp)); CHECK(result.starts_with(expected_year)); - // Human verification - fmt::print("Current date and time is: {}\n", result); } } WHEN("A filename is generated.") @@ -119,8 +169,6 @@ SCENARIO("Various string/stream/time utilities" * auto const file_suffix = filename.string().find("off"); CHECK_NE(file_suffix, std::string::npos); CHECK_EQ(filename.string().find(':'), std::string::npos); - // Human verification - fmt::print("Filename is: {}\n", filename.string()); } } } @@ -158,17 +206,25 @@ SCENARIO("Reading and writing Delaunay triangulations to files" * // Construct a manifold from a Delaunay triangulation manifolds::Manifold_3 const manifold( foliated_triangulations::FoliatedTriangulation_3(triangulation, 0, 1)); + WHEN("A replayable checkpoint filename is generated") + { + auto const filename = make_filename(manifold, cdt::Random_seed{92}, 7); + THEN("The seed and completed pass are recorded before the OFF suffix") + { + CHECK_NE(filename.string().find("-seed-92-pass-7.off"), + std::string::npos); + } + } WHEN("The triangulation is round-tripped through a file") { - auto const filename = std::filesystem::temp_directory_path() / - "cdt-plusplus-utilities-roundtrip.off"; - std::filesystem::remove(filename); + TemporaryDirectory const directory; + auto const filename = directory.file("roundtrip.off"); write_file(filename, manifold.delaunay_snapshot()); REQUIRE(std::filesystem::exists(filename)); auto triangulation_from_file = utilities::read_file>(filename); - THEN("The file contains the triangulation and can be removed") + THEN("The file contains the original triangulation") { REQUIRE(triangulation_from_file.is_valid(true)); REQUIRE_EQ(triangulation_from_file.dimension(), @@ -181,8 +237,6 @@ SCENARIO("Reading and writing Delaunay triangulations to files" * manifold.N1()); REQUIRE_EQ(triangulation_from_file.number_of_vertices(), manifold.N0()); CHECK_EQ(triangulation_from_file, triangulation); - REQUIRE(std::filesystem::remove(filename)); - CHECK_FALSE(std::filesystem::exists(filename)); } } } @@ -191,15 +245,13 @@ SCENARIO("Reading and writing Delaunay triangulations to files" * SCENARIO("File serialization reports complete failures" * doctest::test_suite("utilities")) { - auto const directory = std::filesystem::temp_directory_path(); + TemporaryDirectory const directory; GIVEN("A serializer that marks its output stream bad.") { - auto const filename = directory / "cdt-plusplus-write-failure.off"; + auto const filename = directory.file("write-failure.off"); auto temporary = filename; temporary += ".tmp"; - std::filesystem::remove(filename); - std::filesystem::remove(temporary); { std::ofstream existing{filename}; existing << "previous-checkpoint"; @@ -216,18 +268,15 @@ SCENARIO("File serialization reports complete failures" * std::istreambuf_iterator{}}; CHECK_EQ(contents, "previous-checkpoint"); CHECK_FALSE(std::filesystem::exists(temporary)); - REQUIRE(std::filesystem::remove(filename)); } } } GIVEN("A serializer that recursively starts another file write.") { - auto const filename = directory / "cdt-plusplus-reentrant-write.off"; + auto const filename = directory.file("reentrant-write.off"); auto temporary = filename; temporary += ".tmp"; - std::filesystem::remove(filename); - std::filesystem::remove(temporary); { std::ofstream existing{filename}; existing << "previous-checkpoint"; @@ -244,15 +293,13 @@ SCENARIO("File serialization reports complete failures" * std::istreambuf_iterator{}}; CHECK_EQ(contents, "previous-checkpoint"); CHECK_FALSE(std::filesystem::exists(temporary)); - REQUIRE(std::filesystem::remove(filename)); } } } GIVEN("A serialized value followed by unexpected trailing input.") { - auto const filename = directory / "cdt-plusplus-trailing-input.off"; - std::filesystem::remove(filename); + auto const filename = directory.file("trailing-input.off"); { std::ofstream output{filename}; output << "42 trailing-data"; @@ -260,19 +307,17 @@ SCENARIO("File serialization reports complete failures" * WHEN("The file is parsed.") { - THEN("The trailing input is reported and the fixture can be removed.") + THEN("The trailing input is reported.") { CHECK_THROWS_AS(read_file(filename), std::filesystem::filesystem_error); - REQUIRE(std::filesystem::remove(filename)); } } } GIVEN("A file containing malformed input.") { - auto const filename = directory / "cdt-plusplus-malformed-input.off"; - std::filesystem::remove(filename); + auto const filename = directory.file("malformed-input.off"); { std::ofstream output{filename}; output << "not-an-integer"; @@ -280,11 +325,10 @@ SCENARIO("File serialization reports complete failures" * WHEN("The file is parsed.") { - THEN("The malformed input is reported and the fixture can be removed.") + THEN("The malformed input is reported.") { CHECK_THROWS_AS(read_file(filename), std::filesystem::filesystem_error); - REQUIRE(std::filesystem::remove(filename)); } } } @@ -297,8 +341,10 @@ SCENARIO("Randomizing functions" * doctest::test_suite("utilities")) { WHEN("We roll a die twice.") { - auto const roll1 = die_roll(); - auto const roll2 = die_roll(); + cdt::Random generator{92}; + CAPTURE(generator.seed()); + auto const roll1 = die_roll(generator); + auto const roll2 = die_roll(generator); THEN("Both results are valid die values.") { CHECK_GE(roll1, 1); @@ -315,7 +361,9 @@ SCENARIO("Randomizing functions" * doctest::test_suite("utilities")) iota(container.begin(), container.end(), 0); WHEN("The container is shuffled.") { - ranges::shuffle(container, make_random_generator()); + cdt::Random generator{92}; + CAPTURE(generator.seed()); + ranges::shuffle(container, generator); THEN("The shuffled result remains a permutation of the input.") { ranges::sort(container); @@ -323,8 +371,6 @@ SCENARIO("Randomizing functions" * doctest::test_suite("utilities")) { CHECK_EQ(container[static_cast(i)], i); } - fmt::print("\nShuffled container verification:\n"); - fmt::print("{}\n", fmt::join(container, " ")); } } } @@ -332,14 +378,16 @@ SCENARIO("Randomizing functions" * doctest::test_suite("utilities")) { WHEN("We generate six random integers within the range.") { + cdt::Random generator{92}; + CAPTURE(generator.seed()); auto constexpr min = 64; auto constexpr max = 6400; - auto const value1 = generate_random_int(min, max); - auto const value2 = generate_random_int(min, max); - auto const value3 = generate_random_int(min, max); - auto const value4 = generate_random_int(min, max); - auto const value5 = generate_random_int(min, max); - auto const value6 = generate_random_int(min, max); + auto const value1 = generate_random_int(generator, min, max); + auto const value2 = generate_random_int(generator, min, max); + auto const value3 = generate_random_int(generator, min, max); + auto const value4 = generate_random_int(generator, min, max); + auto const value5 = generate_random_int(generator, min, max); + auto const value6 = generate_random_int(generator, min, max); array container = {value1, value2, value3, value4, value5, value6}; THEN("They should all fall within the range.") { @@ -355,13 +403,15 @@ SCENARIO("Randomizing functions" * doctest::test_suite("utilities")) { WHEN("We generate six timeslices within the range.") { + cdt::Random generator{92}; + CAPTURE(generator.seed()); auto constexpr max = 256; - auto const value1 = generate_random_timeslice(max); - auto const value2 = generate_random_timeslice(max); - auto const value3 = generate_random_timeslice(max); - auto const value4 = generate_random_timeslice(max); - auto const value5 = generate_random_timeslice(max); - auto const value6 = generate_random_timeslice(max); + auto const value1 = generate_random_timeslice(generator, max); + auto const value2 = generate_random_timeslice(generator, max); + auto const value3 = generate_random_timeslice(generator, max); + auto const value4 = generate_random_timeslice(generator, max); + auto const value5 = generate_random_timeslice(generator, max); + auto const value6 = generate_random_timeslice(generator, max); array container = {value1, value2, value3, value4, value5, value6}; THEN("They should all fall within the range.") { @@ -378,9 +428,11 @@ SCENARIO("Randomizing functions" * doctest::test_suite("utilities")) { WHEN("We generate a random real number.") { + cdt::Random generator{92}; + CAPTURE(generator.seed()); auto constexpr min = 0.0L; auto constexpr max = 1.0L; - auto const value = generate_random_real(min, max); + auto const value = generate_random_real(generator, min, max); THEN("The real number should lie within that range.") { REQUIRE_LE(min, value); @@ -392,12 +444,14 @@ SCENARIO("Randomizing functions" * doctest::test_suite("utilities")) { WHEN("We generate six probabilities.") { - auto const value1 = generate_probability(); - auto const value2 = generate_probability(); - auto const value3 = generate_probability(); - auto const value4 = generate_probability(); - auto const value5 = generate_probability(); - auto const value6 = generate_probability(); + cdt::Random generator{92}; + CAPTURE(generator.seed()); + auto const value1 = generate_probability(generator); + auto const value2 = generate_probability(generator); + auto const value3 = generate_probability(generator); + auto const value4 = generate_probability(generator); + auto const value5 = generate_probability(generator); + auto const value6 = generate_probability(generator); array container = {value1, value2, value3, value4, value5, value6}; THEN("They should all be valid probabilities.") diff --git a/tests/Vertex_test.cpp b/tests/Vertex_test.cpp index 01d4063819..1182133fed 100644 --- a/tests/Vertex_test.cpp +++ b/tests/Vertex_test.cpp @@ -87,12 +87,7 @@ SCENARIO("Vertex operations" * doctest::test_suite("vertex")) CHECK_EQ(rebuilt.dimensionality(), 0); } - THEN("The vertex is valid.") - { - fmt::print("When a causal vertex is inserted, the vertices are:\n"); - manifold.print_vertices(); - CHECK(manifold.check_vertices()); - } + THEN("The vertex is valid.") { CHECK(manifold.check_vertices()); } } AND_WHEN("Two vertices are inserted.") @@ -116,12 +111,7 @@ SCENARIO("Vertex operations" * doctest::test_suite("vertex")) THEN("A 2 vertex manifold has dimension 1.") { REQUIRE_EQ(manifold.dimensionality(), 1); } - THEN("The vertices are valid.") - { - fmt::print("When 2 causal vertices are inserted, the vertices are:\n"); - manifold.print_vertices(); - CHECK(manifold.check_vertices()); - } + THEN("The vertices are valid.") { CHECK(manifold.check_vertices()); } } AND_WHEN("Three vertices are inserted.") @@ -148,12 +138,7 @@ SCENARIO("Vertex operations" * doctest::test_suite("vertex")) THEN("A 3 vertex manifold has dimension 2.") { REQUIRE_EQ(manifold.dimensionality(), 2); } - THEN("The vertices are valid.") - { - fmt::print("When 3 causal vertices are inserted, the vertices are:\n"); - manifold.print_vertices(); - CHECK(manifold.check_vertices()); - } + THEN("The vertices are valid.") { CHECK(manifold.check_vertices()); } } AND_WHEN("Four vertices are inserted.") @@ -181,13 +166,7 @@ SCENARIO("Vertex operations" * doctest::test_suite("vertex")) THEN("A 4 vertex manifold has dimension 3.") { REQUIRE_EQ(manifold.dimensionality(), 3); } - THEN("The vertices are valid.") - { - fmt::print( - "When 4 causal vertices are inserted, there is a simplex:\n"); - manifold.print_cells(); - CHECK(manifold.check_vertices()); - } + THEN("The vertices are valid.") { CHECK(manifold.check_vertices()); } } AND_WHEN("Five vertices are inserted.") @@ -216,13 +195,7 @@ SCENARIO("Vertex operations" * doctest::test_suite("vertex")) THEN("A 5 vertex manifold still has dimension 3.") { REQUIRE_EQ(manifold.dimensionality(), 3); } - THEN("The vertices are valid.") - { - fmt::print( - "When 5 causal vertices are inserted, there are 2 simplices:\n"); - manifold.print_cells(); - CHECK(manifold.check_vertices()); - } + THEN("The vertices are valid.") { CHECK(manifold.check_vertices()); } } } } diff --git a/tests/main.cpp b/tests/main.cpp index 8af31242af..65c8871d03 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -10,5 +10,4 @@ /// @details Main doctest driver #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN -#define DOCTEST_CONFIG_SUPER_FAST_ASSERTS #include diff --git a/tests/semgrep/doctest_hygiene.cpp b/tests/semgrep/doctest_hygiene.cpp new file mode 100644 index 0000000000..a96385d092 --- /dev/null +++ b/tests/semgrep/doctest_hygiene.cpp @@ -0,0 +1,38 @@ +// ruleid: cdt.cpp.doctest-framework-is-test-only +#include + +// ruleid: cdt.cpp.doctest-framework-is-test-only +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN + +// ruleid: cdt.cpp.doctest-framework-is-test-only +SCENARIO("Production code owns a doctest scenario") {} + +void skipped_scenario() +{ + // ruleid: cdt.cpp.no-skipped-doctests + auto const decorator = doctest::skip(); +} + +void diagnostic_only_output(Printable& printable) +{ + // ruleid: cdt.cpp.no-diagnostic-output-in-doctests + fmt::print("intermediate value: {}\n", 42); + // ruleid: cdt.cpp.no-diagnostic-output-in-doctests + std::cout << "intermediate value: " << 42 << '\n'; + // ruleid: cdt.cpp.no-diagnostic-output-in-doctests + printable.print_results(); +} + +void asserted_output(std::ostream& output) +{ + // ok: cdt.cpp.no-diagnostic-output-in-doctests + output << "captured value: " << 42; +} + +void temporary_files(std::string const& generated_name) +{ + // ruleid: cdt.cpp.test-temp-paths-are-unique + auto const fixed = std::filesystem::temp_directory_path() / "cdt-output.txt"; + // ok: cdt.cpp.test-temp-paths-are-unique + auto const unique = std::filesystem::temp_directory_path() / generated_name; +} diff --git a/tests/semgrep/move_hot_path_logging.cpp b/tests/semgrep/move_hot_path_logging.cpp new file mode 100644 index 0000000000..df8745f8e3 --- /dev/null +++ b/tests/semgrep/move_hot_path_logging.cpp @@ -0,0 +1,19 @@ +void routine_move_diagnostics() +{ + // ruleid: cdt.cpp.no-routine-output-in-move-hot-paths + spdlog::trace("proposal started"); + // ruleid: cdt.cpp.no-routine-output-in-move-hot-paths + spdlog::debug("proposal rejected"); + // ruleid: cdt.cpp.no-routine-output-in-move-hot-paths + spdlog::info("move applied"); + // ruleid: cdt.cpp.no-routine-output-in-move-hot-paths + fmt::print("move applied\n"); +} + +void invariant_diagnostics() +{ + // ok: cdt.cpp.no-routine-output-in-move-hot-paths + spdlog::warn("incident cells require repair"); + // ok: cdt.cpp.no-routine-output-in-move-hot-paths + spdlog::error("move violated a topology invariant"); +} diff --git a/tests/semgrep/random_ownership.cpp b/tests/semgrep/random_ownership.cpp new file mode 100644 index 0000000000..5310a29e5b --- /dev/null +++ b/tests/semgrep/random_ownership.cpp @@ -0,0 +1,30 @@ +void hidden_stochastic_state() +{ + // ruleid: cdt.cpp.random-entropy-is-centralized + std::random_device entropy; + // ruleid: cdt.cpp.random-entropy-is-centralized + std::random_device braced_entropy{}; + // ruleid: cdt.cpp.use-repository-random-abstraction + pcg64 engine; + // ruleid: cdt.cpp.use-repository-random-abstraction + pcg32 seeded_engine{42}; +} + +void caller_owned_random(cdt::Random& random) +{ + // ok: cdt.cpp.random-entropy-is-centralized + // ok: cdt.cpp.use-repository-random-abstraction + auto const value = random(); +} + +void hidden_wrapper_entropy() +{ + // ruleid: cdt.cpp.no-hidden-random-construction + cdt::Random random; + // ruleid: cdt.cpp.no-hidden-random-construction + cdt::Random braced_random{}; + // ruleid: cdt.cpp.no-hidden-random-construction + auto temporary_random = cdt::Random{}; + // ok: cdt.cpp.no-hidden-random-construction + cdt::Random seeded_random{92}; +} diff --git a/uv.lock b/uv.lock index 8ff8650d5c..95dc0ba23f 100644 --- a/uv.lock +++ b/uv.lock @@ -11,6 +11,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/0a/a10b45aab35b175aded078a462dc8d0c698f5b13946e7cb0869097b78bb6/absl_py-2.5.0-py3-none-any.whl", hash = "sha256:0f17b89f2a4eaaedc4f28c622998aa690564b3012a396a4ffad0821007fe03ba", size = 137410, upload-time = "2026-07-03T10:57:46.735Z" }, ] +[[package]] +name = "actionlint-py" +version = "1.7.12.24" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/0b/3f29683dfbe94208fb5c3806806a6ef419972892e25c3c4f95198f68c978/actionlint_py-1.7.12.24.tar.gz", hash = "sha256:7571b0724fde79b2572b98b2b53792c470249d4db29951b57fc49b9cd3eaf11e", size = 12071, upload-time = "2026-03-31T06:21:35.015Z" } + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + [[package]] name = "astunparse" version = "1.6.3" @@ -33,15 +61,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] +[[package]] +name = "boltons" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/1f/6c0608d86e0fc77c982a2923ece80eef85f091f2332fc13cbce41d70d502/boltons-21.0.0.tar.gz", hash = "sha256:65e70a79a731a7fe6e98592ecfb5ccf2115873d01dbc576079874629e5c90f13", size = 180201, upload-time = "2021-05-17T01:20:17.802Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/a7/1a31561d10a089fcb46fe286766dd4e053a12f6e23b4fd1c26478aff2475/boltons-21.0.0-py2.py3-none-any.whl", hash = "sha256:b9bb7b58b2b420bbe11a6025fdef6d3e5edc9f76a42fb467afe7ca212ef9948b", size = 193723, upload-time = "2021-05-17T01:20:20.023Z" }, +] + +[[package]] +name = "bracex" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/01/5f394b8bcd6e5b92f73130990960423bbb19711f906bd9fe9ea5557c667c/bracex-3.0.1.tar.gz", hash = "sha256:4e38e32392e4a4780fe15d644bfc7c8514057cfc3861e060b11814ce829c25e4", size = 44019, upload-time = "2026-07-20T13:43:00.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/8f/6f7273a7adb8d73fc8d21ede4376a3e475e52f98435c6007f69100dec8ca/bracex-3.0.1-py3-none-any.whl", hash = "sha256:6523ad83aeb5098a4ee597cff0f964442ff74e460bd3fafaffab6a013ff2288c", size = 11940, upload-time = "2026-07-20T13:42:59.268Z" }, +] + [[package]] name = "cdt-plusplus-scripts" -version = "0.0.0" -source = { virtual = "." } +version = "1.0.0rc1" +source = { editable = "." } [package.dev-dependencies] dev = [ + { name = "actionlint-py" }, + { name = "clang-format" }, + { name = "pyyaml" }, { name = "ruff" }, + { name = "semgrep" }, { name = "ty" }, + { name = "yamllint" }, ] experiments = [ { name = "comet-ml" }, @@ -54,8 +105,13 @@ experiments = [ [package.metadata.requires-dev] dev = [ + { name = "actionlint-py", specifier = "==1.7.12.24" }, + { name = "clang-format", specifier = "==22.1.8" }, + { name = "pyyaml", specifier = "==6.0.3" }, { name = "ruff", specifier = "==0.15.21" }, + { name = "semgrep", specifier = "==1.169.0" }, { name = "ty", specifier = "==0.0.59" }, + { name = "yamllint", specifier = "==1.38.0" }, ] experiments = [ { name = "comet-ml", specifier = ">=3.58.3,<4" }, @@ -73,6 +129,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] +[[package]] +name = "cffi" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, + { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, + { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.9" @@ -95,6 +174,64 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, ] +[[package]] +name = "clang-format" +version = "22.1.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/55/b48aba45ba2638a706df1680e3dcdf96f94aed8e884886192d411d7a0071/clang_format-22.1.8.tar.gz", hash = "sha256:61a23f4fc0ad1932e1b0300ca451108bf1e751eb89f478062d87f888db9190e6", size = 11508, upload-time = "2026-07-11T14:08:34.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/d8/29b9db6098da1a011ca3f7560c3942fa81404dbbb4367c3bd1d5c435da3b/clang_format-22.1.8-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:fc2ac5bd0ea41af49968fb69426207806d5f7016cb8f4bfbd44f4f1ffe8d53f2", size = 1492639, upload-time = "2026-07-11T14:08:10.531Z" }, + { url = "https://files.pythonhosted.org/packages/2e/55/539cc1036dae16659f50500ca34838cc5b16cd3e98e3faaf164186b98093/clang_format-22.1.8-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:d1147107222c0dda3e4869e9e8c4a79f9ed1de83819e5274de42b82adf3d2129", size = 1482323, upload-time = "2026-07-11T14:08:12.076Z" }, + { url = "https://files.pythonhosted.org/packages/50/25/a9734da014eecc1f54c051ad643a28f2f6643dcc812ac59320e80e2b1a3b/clang_format-22.1.8-py2.py3-none-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48c3b8dcfe9d4e964ced0e744e0f1f8ddc711bce92e50f6cab21e10f54857d08", size = 1757825, upload-time = "2026-07-11T14:08:13.397Z" }, + { url = "https://files.pythonhosted.org/packages/5d/19/76bf4dfba7d418f3da4fe89ace66856abd79c8c314e750d5f0fb754de6f5/clang_format-22.1.8-py2.py3-none-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:07312f8a74bda89b6ce32fae46c589cd7bb210a5d3fb2829a70e28ff449f5b80", size = 1888650, upload-time = "2026-07-11T14:08:14.722Z" }, + { url = "https://files.pythonhosted.org/packages/5c/b0/25fb71006b581c4e1dc4680b61c61842836dd1adb860e02f898deed8a919/clang_format-22.1.8-py2.py3-none-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6d7382dd728431c0cecf3c7db6ac0904e162c7c18889696ab8dc0630ff9349b", size = 2070288, upload-time = "2026-07-11T14:08:15.98Z" }, + { url = "https://files.pythonhosted.org/packages/36/94/f7c185f2f7c9cbd60878a260a2a03cc4eef0b9b9fbe8eb5045fa9b5ccbe9/clang_format-22.1.8-py2.py3-none-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7763ce0f45b5ff0b5ca7a830d6061ce431dcb644211f9b2bb483bfa66d6c78ca", size = 2101885, upload-time = "2026-07-11T14:08:17.329Z" }, + { url = "https://files.pythonhosted.org/packages/e5/88/b82c066fa807da4ca2518fecf79071361f6324b77375e5e92c059c0697fd/clang_format-22.1.8-py2.py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b00cff6bfd1f1686f073a4fdf1cb937dbd58bf7510c659477805c03afdea0816", size = 1841177, upload-time = "2026-07-11T14:08:18.828Z" }, + { url = "https://files.pythonhosted.org/packages/03/7c/996fc84930d96db84418fbd16d3935ba77f42f9078d7610e4eae3e0e9294/clang_format-22.1.8-py2.py3-none-manylinux_2_31_armv7l.whl", hash = "sha256:396f66b2237131ae7c5c9a9c34d50b1b9bee60ec6754e8b0dcb943850c747aa6", size = 1684807, upload-time = "2026-07-11T14:08:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/1b/42/423de2ddedd3e068f7b773d7a4a593266692bc6108a2dd8ad4403d2ae718/clang_format-22.1.8-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:02ff8ad2e6a60554cc6b9b34310f71adb04da39e94302e08e3a655b77e4dd31c", size = 2736272, upload-time = "2026-07-11T14:08:21.625Z" }, + { url = "https://files.pythonhosted.org/packages/c1/6b/fbb98122d35333012d47186d669c1bb9a1cb527faa9a4a12c43124662f8f/clang_format-22.1.8-py2.py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8fc6139349a64f82d24396c815f5584f56da8912482dceadd9e5d7a3f8c17d6c", size = 2515477, upload-time = "2026-07-11T14:08:23.166Z" }, + { url = "https://files.pythonhosted.org/packages/40/99/a5e00402fe8281754df679a1028132a28ea0c0024d363509d9c29944ab85/clang_format-22.1.8-py2.py3-none-musllinux_1_2_i686.whl", hash = "sha256:65df71f1eab12de161b9059483be55cb72490ec7fc7ca4915b5de49722a76a46", size = 2986602, upload-time = "2026-07-11T14:08:24.679Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f7/4a02e8f7d54f71c7ee4ce48c35cbe8f2be1afb1fbbca543f8db43f56a959/clang_format-22.1.8-py2.py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:0223471f781647d476ce0e09a0512521043343313e42932d900f97a15eacb510", size = 3120341, upload-time = "2026-07-11T14:08:26.234Z" }, + { url = "https://files.pythonhosted.org/packages/84/05/6bd5e7bea1679dbed37a94a3b47d2ef0110792f64fd5761b2301d5b34fd2/clang_format-22.1.8-py2.py3-none-musllinux_1_2_s390x.whl", hash = "sha256:41e00922840376f8d1239db8fb5bc6b1d313bd09752344a85b0f8071112e5c7e", size = 3217448, upload-time = "2026-07-11T14:08:27.799Z" }, + { url = "https://files.pythonhosted.org/packages/56/e3/3fcba146f12b9fbad24034136718d113dfacac03b1cf8273b2aa7bd641b0/clang_format-22.1.8-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:734d22be5c9d3a72a841444817aae8168c8f4bdccb08de491f05673e42ec7304", size = 2848689, upload-time = "2026-07-11T14:08:29.283Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2b/5a7f2fba71179331b51bb547a02808a56aa515f8637adf035fd34c1d3b0b/clang_format-22.1.8-py2.py3-none-win32.whl", hash = "sha256:a796192453ae56c61e975fc56ee7defb4187013fc3de798cbe608cfe326762aa", size = 1298699, upload-time = "2026-07-11T14:08:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/08/60/c6783b3190a8f741107a44912a11c39c1a51e254e86a4c43cb0151cea0dd/clang_format-22.1.8-py2.py3-none-win_amd64.whl", hash = "sha256:5fe6ad3e9399d589aff5ead432568a84cdcbbd621f1708340819efd74cbf8176", size = 1465069, upload-time = "2026-07-11T14:08:31.935Z" }, + { url = "https://files.pythonhosted.org/packages/43/ca/7e1fa4a6044c37c37356bb18fc938d2811754231e448aaffc192dc3774ce/clang_format-22.1.8-py2.py3-none-win_arm64.whl", hash = "sha256:1fac18f32426c6fd7acde7087511bd80e2c549b2cd7477099582c216ae82fa63", size = 1344986, upload-time = "2026-07-11T14:08:33.183Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click-option-group" +version = "0.5.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/ff/d291d66595b30b83d1cb9e314b2c9be7cfc7327d4a0d40a15da2416ea97b/click_option_group-0.5.9.tar.gz", hash = "sha256:f94ed2bc4cf69052e0f29592bd1e771a1789bd7bfc482dd0bc482134aff95823", size = 22222, upload-time = "2025-10-09T09:38:01.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/45/54bb2d8d4138964a94bef6e9afe48b0be4705ba66ac442ae7d8a8dc4ffef/click_option_group-0.5.9-py3-none-any.whl", hash = "sha256:ad2599248bd373e2e19bec5407967c3eec1d0d4fc4a5e77b08a0481e75991080", size = 11553, upload-time = "2025-10-09T09:38:00.066Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + [[package]] name = "comet-ml" version = "3.58.4" @@ -152,6 +289,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, ] +[[package]] +name = "cryptography" +version = "49.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, +] + [[package]] name = "cycler" version = "0.12.1" @@ -193,6 +367,27 @@ ini = [ { name = "configobj" }, ] +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883, upload-time = "2024-07-12T22:26:00.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload-time = "2024-07-12T22:25:58.476Z" }, +] + +[[package]] +name = "face" +version = "26.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boltons" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/fd/f84f0600bd72953d5a322f0dedbd4f900e2cedab718e6b6a093ae2d16aae/face-26.0.1.tar.gz", hash = "sha256:8183d94bc248baaea855a9f8445f97a22a9988908e60abddccc6e251da77c4c6", size = 51754, upload-time = "2026-06-17T23:14:39.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/24/0159c48d19c8b05e6969ee809bbbecc5a5c863f7e38c9327e2c63cb06f0f/face-26.0.1-py3-none-any.whl", hash = "sha256:ab0a83c37c9789dce658a67a9a80eafaa113c9ec37c5a9d950ff5480542a062d", size = 57572, upload-time = "2026-06-17T23:14:38.711Z" }, +] + [[package]] name = "flatbuffers" version = "25.12.19" @@ -227,6 +422,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1d/33/f1c6a276de27b7d7339a34749cc33fa87f077f921969c47185d34a887ae2/gast-0.7.0-py3-none-any.whl", hash = "sha256:99cbf1365633a74099f69c59bd650476b96baa5ef196fec88032b00b31ba36f7", size = 22966, upload-time = "2025-11-29T15:30:03.983Z" }, ] +[[package]] +name = "glom" +version = "25.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "boltons" }, + { name = "face" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/74/8387f95565ba7c30cd152a585b275ebb9a834d1d32782425c5d2fe0a102c/glom-25.12.0.tar.gz", hash = "sha256:1ae7da88be3693df40ad27bdf57a765a55c075c86c971bcddd67927403eb0069", size = 196128, upload-time = "2025-12-29T06:29:07.274Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/e6/4129d9a3baa72d747533bb33376543ccadd9a7f9944e5a6e3ae2e245f5d6/glom-25.12.0-py3-none-any.whl", hash = "sha256:b9f21e77f71a6576a43864e85066b8cc3f0f778d0d50961563f8981377a6dcb1", size = 103295, upload-time = "2025-12-29T06:29:06.074Z" }, +] + [[package]] name = "google-pasta" version = "0.2.0" @@ -239,6 +448,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a3/de/c648ef6835192e6e2cc03f40b19eeda4382c49b5bafb43d88b931c4c74ac/google_pasta-0.2.0-py3-none-any.whl", hash = "sha256:b32482794a366b5366a32c92a9a9201b107821889935a02b3e51f6b432ea84ed", size = 57471, upload-time = "2020-03-13T18:57:48.872Z" }, ] +[[package]] +name = "googleapis-common-protos" +version = "1.75.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, +] + [[package]] name = "grpcio" version = "1.82.1" @@ -260,6 +481,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/96/4f/a5fe8bf0d0a1b24855f370293075c931f27de4eb55f0f158786095bf3c11/grpcio-1.82.1-cp312-cp312-win_amd64.whl", hash = "sha256:1fa3223a3a2e1db74f4c2b255189eb7ea875dfba56e221d252ee3fc7b204778e", size = 5001580, upload-time = "2026-07-08T12:35:18.689Z" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + [[package]] name = "h5py" version = "3.14.0" @@ -276,6 +506,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7a/6d/6426d5d456f593c94b96fa942a9b3988ce4d65ebaf57d7273e452a7222e8/h5py-3.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:bf4897d67e613ecf5bdfbdab39a1158a64df105827da70ea1d90243d796d367f", size = 2862845, upload-time = "2025-06-06T14:05:23.699Z" }, ] +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + [[package]] name = "idna" version = "3.18" @@ -285,9 +552,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + [[package]] name = "jsonschema" -version = "4.26.0" +version = "4.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -295,9 +574,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, ] [[package]] @@ -413,6 +692,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/11/4d124efbbad677b7b7552f6f85a3bd432d4232f95400cea98fcd2ae36ef3/matplotlib-3.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:480194afceca4df2f137c2721227d3cba67121fbf4397b69cee7f83714b0a58a", size = 9007545, upload-time = "2026-07-18T03:38:32.833Z" }, ] +[[package]] +name = "mcp" +version = "1.23.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/a4/d06a303f45997e266f2c228081abe299bbcba216cb806128e2e49095d25f/mcp-1.23.3.tar.gz", hash = "sha256:b3b0da2cc949950ce1259c7bfc1b081905a51916fcd7c8182125b85e70825201", size = 600697, upload-time = "2025-12-09T16:04:37.351Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/c6/13c1a26b47b3f3a3b480783001ada4268917c9f42d78a079c336da2e75e5/mcp-1.23.3-py3-none-any.whl", hash = "sha256:32768af4b46a1b4f7df34e2bfdf5c6011e7b63d7f1b0e321d0fdef4cd6082031", size = 231570, upload-time = "2025-12-09T16:04:35.56Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -466,6 +770,141 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2b/2a/d1a88066b1c14186f5d3c0d18c94f17b064511982bab0578d49ee9d43c29/numpy-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b", size = 10350488, upload-time = "2026-07-04T17:06:37.785Z" }, ] +[[package]] +name = "opentelemetry-api" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/04/05040d7ce33a907a2a02257e601992f0cdf11c73b33f13c4492bf6c3d6d5/opentelemetry_api-1.37.0.tar.gz", hash = "sha256:540735b120355bd5112738ea53621f8d5edb35ebcd6fe21ada3ab1c61d1cd9a7", size = 64923, upload-time = "2025-09-11T10:29:01.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/48/28ed9e55dcf2f453128df738210a980e09f4e468a456fa3c763dbc8be70a/opentelemetry_api-1.37.0-py3-none-any.whl", hash = "sha256:accf2024d3e89faec14302213bc39550ec0f4095d1cf5ca688e1bfb1c8612f47", size = 65732, upload-time = "2025-09-11T10:28:41.826Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/6c/10018cbcc1e6fff23aac67d7fd977c3d692dbe5f9ef9bb4db5c1268726cc/opentelemetry_exporter_otlp_proto_common-1.37.0.tar.gz", hash = "sha256:c87a1bdd9f41fdc408d9cc9367bb53f8d2602829659f2b90be9f9d79d0bfe62c", size = 20430, upload-time = "2025-09-11T10:29:03.605Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/13/b4ef09837409a777f3c0af2a5b4ba9b7af34872bc43609dda0c209e4060d/opentelemetry_exporter_otlp_proto_common-1.37.0-py3-none-any.whl", hash = "sha256:53038428449c559b0c564b8d718df3314da387109c4d36bd1b94c9a641b0292e", size = 18359, upload-time = "2025-09-11T10:28:44.939Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/e3/6e320aeb24f951449e73867e53c55542bebbaf24faeee7623ef677d66736/opentelemetry_exporter_otlp_proto_http-1.37.0.tar.gz", hash = "sha256:e52e8600f1720d6de298419a802108a8f5afa63c96809ff83becb03f874e44ac", size = 17281, upload-time = "2025-09-11T10:29:04.844Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/e9/70d74a664d83976556cec395d6bfedd9b85ec1498b778367d5f93e373397/opentelemetry_exporter_otlp_proto_http-1.37.0-py3-none-any.whl", hash = "sha256:54c42b39945a6cc9d9a2a33decb876eabb9547e0dcb49df090122773447f1aef", size = 19576, upload-time = "2025-09-11T10:28:46.726Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.58b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/36/7c307d9be8ce4ee7beb86d7f1d31027f2a6a89228240405a858d6e4d64f9/opentelemetry_instrumentation-0.58b0.tar.gz", hash = "sha256:df640f3ac715a3e05af145c18f527f4422c6ab6c467e40bd24d2ad75a00cb705", size = 31549, upload-time = "2025-09-11T11:42:14.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/db/5ff1cd6c5ca1d12ecf1b73be16fbb2a8af2114ee46d4b0e6d4b23f4f4db7/opentelemetry_instrumentation-0.58b0-py3-none-any.whl", hash = "sha256:50f97ac03100676c9f7fc28197f8240c7290ca1baa12da8bfbb9a1de4f34cc45", size = 33019, upload-time = "2025-09-11T11:41:00.624Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-requests" +version = "0.58b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/42/83ee32de763b919779aaa595b60c5a7b9c0a4b33952bbe432c5f6a783085/opentelemetry_instrumentation_requests-0.58b0.tar.gz", hash = "sha256:ae9495e6ff64e27bdb839fce91dbb4be56e325139828e8005f875baf41951a2e", size = 15188, upload-time = "2025-09-11T11:42:51.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/4d/f3476b28ea167d1762134352d01ae9693940a42c78994d9f1b32a4477816/opentelemetry_instrumentation_requests-0.58b0-py3-none-any.whl", hash = "sha256:672a0be0bb5b52bea0c11820b35e27edcf4cd22d34abe4afc59a92a80519f8a8", size = 12966, upload-time = "2025-09-11T11:41:52.67Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-threading" +version = "0.58b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/a9/3888cb0470e6eb48ea17b6802275ae71df411edd6382b9a8e8f391936fda/opentelemetry_instrumentation_threading-0.58b0.tar.gz", hash = "sha256:f68c61f77841f9ff6270176f4d496c10addbceacd782af434d705f83e4504862", size = 8770, upload-time = "2025-09-11T11:42:56.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/54/add1076cb37980e617723a96e29c84006983e8ad6fc589dde7f69ddc57d4/opentelemetry_instrumentation_threading-0.58b0-py3-none-any.whl", hash = "sha256:eacc072881006aceb5b9b6831bcdce718c67ef6f31ac0b32bd6a23a94d979b4a", size = 9312, upload-time = "2025-09-11T11:41:58.603Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/ea/a75f36b463a36f3c5a10c0b5292c58b31dbdde74f6f905d3d0ab2313987b/opentelemetry_proto-1.37.0.tar.gz", hash = "sha256:30f5c494faf66f77faeaefa35ed4443c5edb3b0aa46dad073ed7210e1a789538", size = 46151, upload-time = "2025-09-11T10:29:11.04Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/25/f89ea66c59bd7687e218361826c969443c4fa15dfe89733f3bf1e2a9e971/opentelemetry_proto-1.37.0-py3-none-any.whl", hash = "sha256:8ed8c066ae8828bbf0c39229979bdf583a126981142378a9cbe9d6fd5701c6e2", size = 72534, upload-time = "2025-09-11T10:28:56.831Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/62/2e0ca80d7fe94f0b193135375da92c640d15fe81f636658d2acf373086bc/opentelemetry_sdk-1.37.0.tar.gz", hash = "sha256:cc8e089c10953ded765b5ab5669b198bbe0af1b3f89f1007d19acd32dc46dda5", size = 170404, upload-time = "2025-09-11T10:29:11.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/62/9f4ad6a54126fb00f7ed4bb5034964c6e4f00fcd5a905e115bd22707e20d/opentelemetry_sdk-1.37.0-py3-none-any.whl", hash = "sha256:8f3c3c22063e52475c5dbced7209495c2c16723d016d39287dfc215d1771257c", size = 131941, upload-time = "2025-09-11T10:28:57.83Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.58b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/1b/90701d91e6300d9f2fb352153fb1721ed99ed1f6ea14fa992c756016e63a/opentelemetry_semantic_conventions-0.58b0.tar.gz", hash = "sha256:6bd46f51264279c433755767bb44ad00f1c9e2367e1b42af563372c5a6fa0c25", size = 129867, upload-time = "2025-09-11T10:29:12.597Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/90/68152b7465f50285d3ce2481b3aec2f82822e3f52e5152eeeaf516bab841/opentelemetry_semantic_conventions-0.58b0-py3-none-any.whl", hash = "sha256:5564905ab1458b96684db1340232729fce3b5375a06e140e8904c78e4f815b28", size = 207954, upload-time = "2025-09-11T10:28:59.218Z" }, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.58b0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/5f/02f31530faf50ef8a41ab34901c05cbbf8e9d76963ba2fb852b0b4065f4e/opentelemetry_util_http-0.58b0.tar.gz", hash = "sha256:de0154896c3472c6599311c83e0ecee856c4da1b17808d39fdc5cce5312e4d89", size = 9411, upload-time = "2025-09-11T11:43:05.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/a3/0a1430c42c6d34d8372a16c104e7408028f0c30270d8f3eb6cccf2e82934/opentelemetry_util_http-0.58b0-py3-none-any.whl", hash = "sha256:6c6b86762ed43025fbd593dc5f700ba0aa3e09711aedc36fd48a13b23d8cb1e7", size = 7652, upload-time = "2025-09-11T11:42:09.682Z" }, +] + [[package]] name = "opt-einsum" version = "3.4.0" @@ -506,6 +945,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "peewee" +version = "3.19.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/b0/79462b42e89764998756e0557f2b58a15610a5b4512fbbcccae58fba7237/peewee-3.19.0.tar.gz", hash = "sha256:f88292a6f0d7b906cb26bca9c8599b8f4d8920ebd36124400d0cbaaaf915511f", size = 974035, upload-time = "2026-01-07T17:24:59.597Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/41/19c65578ef9a54b3083253c68a607f099642747168fe00f3a2bceb7c3a34/peewee-3.19.0-py3-none-any.whl", hash = "sha256:de220b94766e6008c466e00ce4ba5299b9a832117d9eb36d45d0062f3cfd7417", size = 411885, upload-time = "2026-01-07T17:24:58.33Z" }, +] + [[package]] name = "pillow" version = "12.3.0" @@ -525,17 +982,17 @@ wheels = [ [[package]] name = "protobuf" -version = "7.35.1" +version = "6.33.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" }, - { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, - { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, - { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, - { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" }, - { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" }, - { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, ] [[package]] @@ -554,6 +1011,74 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, ] +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -563,6 +1088,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pyjwt" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + [[package]] name = "pyparsing" version = "3.3.2" @@ -593,6 +1132,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, +] + [[package]] name = "referencing" version = "0.37.0" @@ -670,6 +1255,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, ] +[[package]] +name = "ruamel-yaml" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, +] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/97/60fda20e2fb54b83a61ae14648b0817c8f5d84a3821e40bfbdae1437026a/ruamel_yaml_clib-0.2.15.tar.gz", hash = "sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600", size = 225794, upload-time = "2025-11-16T16:12:59.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/4b/5fde11a0722d676e469d3d6f78c6a17591b9c7e0072ca359801c4bd17eee/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb15a2e2a90c8475df45c0949793af1ff413acfb0a716b8b94e488ea95ce7cff", size = 149088, upload-time = "2025-11-16T16:13:22.836Z" }, + { url = "https://files.pythonhosted.org/packages/85/82/4d08ac65ecf0ef3b046421985e66301a242804eb9a62c93ca3437dc94ee0/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64da03cbe93c1e91af133f5bec37fd24d0d4ba2418eaf970d7166b0a26a148a2", size = 134553, upload-time = "2025-11-16T16:13:24.151Z" }, + { url = "https://files.pythonhosted.org/packages/b9/cb/22366d68b280e281a932403b76da7a988108287adff2bfa5ce881200107a/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f6d3655e95a80325b84c4e14c080b2470fe4f33b6846f288379ce36154993fb1", size = 737468, upload-time = "2025-11-16T20:22:47.335Z" }, + { url = "https://files.pythonhosted.org/packages/71/73/81230babf8c9e33770d43ed9056f603f6f5f9665aea4177a2c30ae48e3f3/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71845d377c7a47afc6592aacfea738cc8a7e876d586dfba814501d8c53c1ba60", size = 753349, upload-time = "2025-11-16T16:13:26.269Z" }, + { url = "https://files.pythonhosted.org/packages/61/62/150c841f24cda9e30f588ef396ed83f64cfdc13b92d2f925bb96df337ba9/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e5499db1ccbc7f4b41f0565e4f799d863ea720e01d3e99fa0b7b5fcd7802c9", size = 788211, upload-time = "2025-11-16T16:13:27.441Z" }, + { url = "https://files.pythonhosted.org/packages/30/93/e79bd9cbecc3267499d9ead919bd61f7ddf55d793fb5ef2b1d7d92444f35/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4b293a37dc97e2b1e8a1aec62792d1e52027087c8eea4fc7b5abd2bdafdd6642", size = 743203, upload-time = "2025-11-16T16:13:28.671Z" }, + { url = "https://files.pythonhosted.org/packages/8d/06/1eb640065c3a27ce92d76157f8efddb184bd484ed2639b712396a20d6dce/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:512571ad41bba04eac7268fe33f7f4742210ca26a81fe0c75357fa682636c690", size = 747292, upload-time = "2025-11-16T20:22:48.584Z" }, + { url = "https://files.pythonhosted.org/packages/a5/21/ee353e882350beab65fcc47a91b6bdc512cace4358ee327af2962892ff16/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5e9f630c73a490b758bf14d859a39f375e6999aea5ddd2e2e9da89b9953486a", size = 771624, upload-time = "2025-11-16T16:13:29.853Z" }, + { url = "https://files.pythonhosted.org/packages/57/34/cc1b94057aa867c963ecf9ea92ac59198ec2ee3a8d22a126af0b4d4be712/ruamel_yaml_clib-0.2.15-cp312-cp312-win32.whl", hash = "sha256:f4421ab780c37210a07d138e56dd4b51f8642187cdfb433eb687fe8c11de0144", size = 100342, upload-time = "2025-11-16T16:13:31.067Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e5/8925a4208f131b218f9a7e459c0d6fcac8324ae35da269cb437894576366/ruamel_yaml_clib-0.2.15-cp312-cp312-win_amd64.whl", hash = "sha256:2b216904750889133d9222b7b873c199d48ecbb12912aca78970f84a5aa1a4bc", size = 119013, upload-time = "2025-11-16T16:13:32.164Z" }, +] + [[package]] name = "ruff" version = "0.15.21" @@ -704,6 +1316,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" }, ] +[[package]] +name = "semgrep" +version = "1.169.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "boltons" }, + { name = "click" }, + { name = "click-option-group" }, + { name = "colorama" }, + { name = "exceptiongroup" }, + { name = "glom" }, + { name = "jsonschema" }, + { name = "mcp" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-instrumentation-requests" }, + { name = "opentelemetry-instrumentation-threading" }, + { name = "opentelemetry-sdk" }, + { name = "packaging" }, + { name = "peewee" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "rich" }, + { name = "ruamel-yaml" }, + { name = "ruamel-yaml-clib" }, + { name = "semantic-version" }, + { name = "tomli" }, + { name = "typing-extensions" }, + { name = "urllib3" }, + { name = "wcmatch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/0c/3f0bd4d2fac226c2c3f9acc4d8782806e0d593a14e22b2f16e12156a9a94/semgrep-1.169.0.tar.gz", hash = "sha256:46932f875b8dff4cb731cd4c908443a0f2f585edbb0a5baa92c4fc033246fdea", size = 499932, upload-time = "2026-07-10T16:49:23.956Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/53/c64cc34ce1c9a41d69638cf0ea41108fe0ae517cf38aaafb8e50efe88f6a/semgrep-1.169.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-macosx_10_14_x86_64.whl", hash = "sha256:b8c776de8de61aeb59a5cd479276225a2325bf1195e4a4af1f9530e76bb5f827", size = 45013963, upload-time = "2026-07-10T16:50:39.515Z" }, + { url = "https://files.pythonhosted.org/packages/a8/be/723abdc06372373ebb40ffc922d97570c2e421f39173147e45805d438bf0/semgrep-1.169.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-macosx_11_0_arm64.whl", hash = "sha256:41c366ab1ecd04b5e55c8e2b67e0dfa50a744d79eb09be962353a8b98242b878", size = 49033137, upload-time = "2026-07-10T16:50:42.63Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b6/03559145888b9b99a411df90e54f321d9d012ef58dc541afd2e0a3916b45/semgrep-1.169.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-manylinux_2_34_aarch64.whl", hash = "sha256:ffc783021040bba9784289bcedf8719d7f5c52c73eee3b8bd1814e21422837ac", size = 70906982, upload-time = "2026-07-10T16:50:45.681Z" }, + { url = "https://files.pythonhosted.org/packages/3c/c0/e76a28610aa5a0c2e3fb98cc3438009343013642e1d03563e5d4d68ae5c9/semgrep-1.169.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-manylinux_2_34_x86_64.whl", hash = "sha256:48d899e7e31803fcbf69e69a9876a7a1ade2eddb4ef85828b6576eaa0b4940da", size = 68766411, upload-time = "2026-07-10T16:50:49.386Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4c/b1a95e8eb5e57ba1f8a7a8e9febf2fcc80f61a118f431ad1302cc449b644/semgrep-1.169.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-musllinux_1_2_aarch64.whl", hash = "sha256:ad0c8cb56f3e9ce5ac81795fc3395fd05d9a65726a2492d4ec64190ec5816911", size = 77653156, upload-time = "2026-07-10T16:50:52.769Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/12b3fbf368da3eb7ed5ce760bf002aea5b6404f9e42155d3904dd02708e6/semgrep-1.169.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-musllinux_1_2_x86_64.whl", hash = "sha256:c5ffdf474a40302281af43cc2f2eefb31de5f46a6dbffcc2f4099638ed3cbfa2", size = 75167351, upload-time = "2026-07-10T16:50:56.469Z" }, + { url = "https://files.pythonhosted.org/packages/d8/6a/519c3b25dd3cb92659e9b97aa8070fe6d76093ebbb62030612f8e7d99622/semgrep-1.169.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-win_amd64.whl", hash = "sha256:54336981cab97b95a5f06694b2f10a36f0af86738bdc9a2050fa1865d2e44d06", size = 56940387, upload-time = "2026-07-10T16:51:00.493Z" }, +] + [[package]] name = "sentry-sdk" version = "2.66.0" @@ -755,6 +1411,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "sse-starlette" +version = "3.4.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/10/a34c656829ffc1c4b22ef36d70d9ebb6b99c020e2aeb17cee5485099f028/sse_starlette-3.4.6.tar.gz", hash = "sha256:725f8a1bd6d26ae1b2c9610c0ef5065dfdd496f3988d28adcf8c4b49dc25c627", size = 32542, upload-time = "2026-07-20T14:16:32.201Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/36/e10c1d1b7ca881d2625db2ec28508578499187bb1c389952c398474e1834/sse_starlette-3.4.6-py3-none-any.whl", hash = "sha256:56217ab4c9a9f9c5db7b21e08732d3e7c2b807f45231ad23de0551a24c4a41f6", size = 16516, upload-time = "2026-07-20T14:16:30.978Z" }, +] + +[[package]] +name = "starlette" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, +] + [[package]] name = "tensorflow" version = "2.21.0" @@ -797,6 +1479,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, ] +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + [[package]] name = "ty" version = "0.0.59" @@ -831,6 +1531,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + [[package]] name = "urllib3" version = "2.7.0" @@ -840,6 +1552,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] +[[package]] +name = "uvicorn" +version = "0.51.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/65/b7c6c443ccc58678c91e1e973bbe2a878591538655d6e1d47f24ba1c51f3/uvicorn-0.51.0.tar.gz", hash = "sha256:f6f4b69b657c312f516dd2d268ab9ae6f254b11e4bac504f37b2ab58b24dd0b0", size = 94412, upload-time = "2026-07-08T10:59:05.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/ec/dbb7e5a6b91f86bfb9eb7d2988a2730907b6a729875b949c7f022e8b88fa/uvicorn-0.51.0-py3-none-any.whl", hash = "sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b", size = 73219, upload-time = "2026-07-08T10:59:04.44Z" }, +] + +[[package]] +name = "wcmatch" +version = "8.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bracex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/c4/55e0d36da61d7b8b2a49fd273e6b296fd5e8471c72ebbe438635d1af3968/wcmatch-8.5.2.tar.gz", hash = "sha256:a70222b86dea82fb382dd87b73278c10756c138bd6f8f714e2183128887b9eb2", size = 114983, upload-time = "2024-05-15T12:51:08.054Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/78/533ef890536e5ba0fd4f7df37482b5800ecaaceae9afc30978a1a7f88ff1/wcmatch-8.5.2-py3-none-any.whl", hash = "sha256:17d3ad3758f9d0b5b4dedc770b65420d4dac62e680229c287bf24c9db856a478", size = 39397, upload-time = "2024-05-15T12:51:06.2Z" }, +] + [[package]] name = "wheel" version = "0.47.0" @@ -854,22 +1591,21 @@ wheels = [ [[package]] name = "wrapt" -version = "2.2.2" +version = "1.17.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/a4/282c8e64300a59fc834518a54bf0afabb4ff9218b5fa76958b450459a844/wrapt-2.2.2.tar.gz", hash = "sha256:0788e321027c999bf221b667bd4a54aaefd1a36283749a860ac3eb77daed0302", size = 129068, upload-time = "2026-06-20T23:49:44.49Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/85/180b40628b23772692a0c76e8030114e1c0ae068470ed531919f0a5f2a4a/wrapt-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8417fd3c674d3c8023d080292d29301531a12daf8bd938dd419710dd2f464f2b", size = 81484, upload-time = "2026-06-20T23:47:59.924Z" }, - { url = "https://files.pythonhosted.org/packages/94/f2/21c90f2a16689702e2aaff45795b11018dff2c9b1242bac10d225483f676/wrapt-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e7070c7472582e31af3dfc2622b2381a0df7435110a9388ed8db5ffbce67efb", size = 82151, upload-time = "2026-06-20T23:48:01.303Z" }, - { url = "https://files.pythonhosted.org/packages/5f/b3/7e6e9fcf4fe7e1b69a49fe6cc5a44e8224bab6283c5233c97e132f14908e/wrapt-2.2.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2e096c9d39a59b35b63c9aacfbbbec2088ff51ff1fc31051acc60a07f42f273a", size = 169828, upload-time = "2026-06-20T23:48:02.719Z" }, - { url = "https://files.pythonhosted.org/packages/0b/43/894f132d857ed5a9904d937baf368badcbe5ea9e436e2f1930fe21c9f1f0/wrapt-2.2.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d1a6050405bf334be33bf66296f113563622972a34900ae6fa60fd283a1a900", size = 171544, upload-time = "2026-06-20T23:48:04.266Z" }, - { url = "https://files.pythonhosted.org/packages/29/de/3c833e03725b477e9ea34028224dd21a48781830101e4e036f77e8b6b102/wrapt-2.2.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10adb01371408c6de504a6658b9886480f1a4919a83752748a387a504a21df79", size = 160663, upload-time = "2026-06-20T23:48:05.708Z" }, - { url = "https://files.pythonhosted.org/packages/33/be/27edce350b24e3054d9d047f65f16d4c4d4c1f3f31c4278a1f8a95c723c8/wrapt-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3442eee2a5798f9b451f1b2cd7518ce8b7e28a2a364696c414460a0e295c012a", size = 169387, upload-time = "2026-06-20T23:48:07.243Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c4/9fd9679af8bf38e146652c7f47b6b352c3e5795b4ad1c0b7f94e15ac2aa7/wrapt-2.2.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6c99012a22f735a85eed7c4b86a3e99c30fdd57d9e115b2b45f796264b58d0bf", size = 158849, upload-time = "2026-06-20T23:48:08.91Z" }, - { url = "https://files.pythonhosted.org/packages/bc/c2/aa6c0c2206803068c6859dabe01f8c84c43744da93d4c67b8946d21655ee/wrapt-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b686cfc008776a3952d6213cb296ed7f45d782a8453936406faa89eac0835ab", size = 168147, upload-time = "2026-06-20T23:48:10.374Z" }, - { url = "https://files.pythonhosted.org/packages/42/63/3eb25da41049d20ae18fcab2dd8b056e02387c4bfa626cbdfb7c3b872e4f/wrapt-2.2.2-cp312-cp312-win32.whl", hash = "sha256:ef2cce266b5b0b07e19fa82e59673b81142b7a3607c8ed1254113d048ed668da", size = 77734, upload-time = "2026-06-20T23:48:11.769Z" }, - { url = "https://files.pythonhosted.org/packages/da/09/0390e008a305360948fa9ce69507d041ac12cb2ee5d28e34467e2ee79391/wrapt-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:abf8c20a2d72ee69e16328b3c91342c446e723bfe48bfcc4dded3b9722ac027f", size = 80585, upload-time = "2026-06-20T23:48:13.117Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b3/84c445c66969f2d3457276b183a48c91097d59bbef9af6c075366b0f8c36/wrapt-2.2.2-cp312-cp312-win_arm64.whl", hash = "sha256:c6c64c5d02578bc4c4bca4f0aef1504de933c1d5b4ac2710b9131111459506c8", size = 79553, upload-time = "2026-06-20T23:48:14.5Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d2/6317eb6d4554855bbf12d61857774af34747bf88a42c19bf306de67e2fa3/wrapt-2.2.2-py3-none-any.whl", hash = "sha256:5bad217350f19ce99ca5b5e71d406765ea86fe541628426772b657375ee1c048", size = 61460, upload-time = "2026-06-20T23:49:42.966Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, ] [[package]] @@ -880,3 +1616,25 @@ sdist = { url = "https://files.pythonhosted.org/packages/33/90/623f99c55c7d0727a wheels = [ { url = "https://files.pythonhosted.org/packages/9a/24/93ce54550a9dd3fd996ed477f00221f215bf6da3580397fbc138d6036e2e/wurlitzer-3.1.1-py3-none-any.whl", hash = "sha256:0b2749c2cde3ef640bf314a9f94b24d929fe1ca476974719a6909dfc568c3aac", size = 8590, upload-time = "2024-06-12T10:27:28.787Z" }, ] + +[[package]] +name = "yamllint" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathspec" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/a0/8fc2d68e132cf918f18273fdc8a1b8432b60d75ac12fdae4b0ef5c9d2e8d/yamllint-1.38.0.tar.gz", hash = "sha256:09e5f29531daab93366bb061e76019d5e91691ef0a40328f04c927387d1d364d", size = 142446, upload-time = "2026-01-13T07:47:53.276Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/92/aed08e68de6e6a3d7c2328ce7388072cd6affc26e2917197430b646aed02/yamllint-1.38.0-py3-none-any.whl", hash = "sha256:fc394a5b3be980a4062607b8fdddc0843f4fa394152b6da21722f5d59013c220", size = 68940, upload-time = "2026-01-13T07:47:51.343Z" }, +] + +[[package]] +name = "zipp" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, +] diff --git a/vcpkg.json b/vcpkg.json index e465afbde5..340e062b45 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,6 +1,6 @@ { "name": "cdt-plusplus", - "version": "0.1.8", + "version": "1.0.0-rc1", "builtin-baseline": "4e82e29f14eac2b3422f18f72c7524d04f19924e", "dependencies": [ "boost-compat", From c508c7a09f95d24bb4c853f8540c093b085646c5 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Mon, 20 Jul 2026 15:33:41 -0700 Subject: [PATCH 02/13] fix(tooling): stabilize cross-platform release validation - run pinned Doxygen and Graphviz checks through just ci on every platform - publish the same validated documentation output to gh-pages - install Windows tooling without Chocolatey - keep sanitizer workloads deterministic and within runner timeouts - ensure Comet experiments clean up and reject malformed initializer output - complete RNG documentation and guard benchmark timing edge cases --- .github/actions/setup-just/action.yml | 2 +- .github/workflows/ci.yml | 61 ++++++++++++++++++- .github/workflows/doxygen.yml | 33 +++++++++- Justfile | 16 ++++- README.md | 48 +++++++-------- docs/Doxyfile | 39 ++++++------ include/Ergodic_moves_3.hpp | 20 +++++- include/Foliated_triangulation.hpp | 20 +++++- include/Formatters.hpp | 35 ++++++----- include/Manifold.hpp | 12 ++++ include/Utilities.hpp | 3 + scripts/doxygen.sh | 74 +++++++++++++++++++++++ scripts/optimize_initialize.py | 12 ++-- scripts/sanitizer.sh | 2 +- scripts/tests/test_optimize_initialize.py | 12 ++++ tests/CMakeLists.txt | 8 ++- tests/Ergodic_moves_3_test.cpp | 1 + tests/Random_benchmark.cpp | 11 +++- tests/S3Action_test.cpp | 4 ++ 19 files changed, 330 insertions(+), 83 deletions(-) create mode 100755 scripts/doxygen.sh diff --git a/.github/actions/setup-just/action.yml b/.github/actions/setup-just/action.yml index fb2fc053ed..1a3136ade1 100644 --- a/.github/actions/setup-just/action.yml +++ b/.github/actions/setup-just/action.yml @@ -36,6 +36,6 @@ runs: echo "version=$version" >> "$GITHUB_OUTPUT" - name: Install Just - uses: taiki-e/cache-cargo-install-action@417450f3c33ee20393705369577571770643d4c7 # v3.0.7 + uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 with: tool: just@${{ steps.resolve.outputs.version }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64801be272..57ba506bdc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,6 +76,10 @@ jobs: shell: bash run: | { + echo "doxygen=$(just --evaluate doxygen_version)" + echo "doxygen-windows-sha256=$(just --evaluate doxygen_windows_sha256)" + echo "graphviz=$(just --evaluate graphviz_version)" + echo "graphviz-windows-sha256=$(just --evaluate graphviz_windows_sha256)" echo "llvm=$(just --evaluate llvm_version)" echo "uv=$(just --evaluate uv_version)" echo "zizmor=$(just --evaluate zizmor_version)" @@ -85,7 +89,60 @@ jobs: if: runner.os != 'Windows' uses: pkgxdev/setup@4d4ae97af87ccb39ab8be4e073dea697fef2c6f7 # v5.0.0 with: - +: llvm.org@${{ steps.tool-versions.outputs.llvm }} + +: | + doxygen.nl@${{ steps.tool-versions.outputs.doxygen }} + graphviz.org@${{ steps.tool-versions.outputs.graphviz }} + llvm.org@${{ steps.tool-versions.outputs.llvm }} + + - name: Install verified documentation tools on Windows + if: runner.os == 'Windows' + shell: pwsh + env: + DOXYGEN_SHA256: ${{ steps.tool-versions.outputs.doxygen-windows-sha256 }} + DOXYGEN_VERSION: ${{ steps.tool-versions.outputs.doxygen }} + GRAPHVIZ_SHA256: ${{ steps.tool-versions.outputs.graphviz-windows-sha256 }} + GRAPHVIZ_VERSION: ${{ steps.tool-versions.outputs.graphviz }} + run: | + $ErrorActionPreference = 'Stop' + + function Assert-ArchiveHash { + param( + [string] $Archive, + [string] $Expected + ) + + $actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $Archive).Hash.ToLowerInvariant() + if ($actual -ne $Expected.ToLowerInvariant()) { + throw "SHA-256 mismatch for $Archive`: expected $Expected, received $actual" + } + } + + $doxygenArchive = Join-Path $env:RUNNER_TEMP 'doxygen.zip' + $doxygenRoot = Join-Path $env:RUNNER_TEMP 'doxygen' + $doxygenTag = 'Release_' + ($env:DOXYGEN_VERSION -replace '\.', '_') + $doxygenBase = 'https://github.com/doxygen/doxygen/releases/download' + $doxygenUrl = "$doxygenBase/$doxygenTag/doxygen-$env:DOXYGEN_VERSION.windows.x64.bin.zip" + Invoke-WebRequest -Uri $doxygenUrl -OutFile $doxygenArchive + Assert-ArchiveHash -Archive $doxygenArchive -Expected $env:DOXYGEN_SHA256 + Expand-Archive -LiteralPath $doxygenArchive -DestinationPath $doxygenRoot + + $graphvizArchive = Join-Path $env:RUNNER_TEMP 'graphviz.zip' + $graphvizRoot = Join-Path $env:RUNNER_TEMP 'graphviz' + $graphvizPackage = "windows_10_cmake_Release_Graphviz-$env:GRAPHVIZ_VERSION-win64.zip" + $graphvizBase = 'https://gitlab.com/api/v4/projects/4207231/packages/generic/graphviz-releases' + $graphvizUrl = "$graphvizBase/$env:GRAPHVIZ_VERSION/$graphvizPackage" + Invoke-WebRequest -Uri $graphvizUrl -OutFile $graphvizArchive + Assert-ArchiveHash -Archive $graphvizArchive -Expected $env:GRAPHVIZ_SHA256 + Expand-Archive -LiteralPath $graphvizArchive -DestinationPath $graphvizRoot + + $doxygen = Get-ChildItem -Path $doxygenRoot -Filter doxygen.exe -Recurse | Select-Object -First 1 + $dot = Get-ChildItem -Path $graphvizRoot -Filter dot.exe -Recurse | Select-Object -First 1 + if ($null -eq $doxygen -or $null -eq $dot) { + throw 'The downloaded documentation archives did not contain doxygen.exe and dot.exe.' + } + + $doxygen.Directory.FullName | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + $dot.Directory.FullName | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - name: Set up Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 @@ -101,7 +158,7 @@ jobs: run: uv sync --locked --group dev - name: Install zizmor - uses: taiki-e/cache-cargo-install-action@417450f3c33ee20393705369577571770643d4c7 # v3.0.7 + uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 with: tool: zizmor@${{ steps.tool-versions.outputs.zizmor }} diff --git a/.github/workflows/doxygen.yml b/.github/workflows/doxygen.yml index 2dd2a0ab15..d520aa970a 100644 --- a/.github/workflows/doxygen.yml +++ b/.github/workflows/doxygen.yml @@ -20,9 +20,36 @@ jobs: permissions: contents: write steps: - - uses: DenverCoder1/doxygen-github-pages-action@a30f9538f8ef1305aeceb563018f452c7a62d200 # v2.0.0 + - name: Check out repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: - github_token: ${{ secrets.GITHUB_TOKEN }} + persist-credentials: false + + - name: Set up Just + uses: ./.github/actions/setup-just + + - name: Resolve documentation tool versions + id: tool-versions + shell: bash + run: | + { + echo "doxygen=$(just --evaluate doxygen_version)" + echo "graphviz=$(just --evaluate graphviz_version)" + } >> "$GITHUB_OUTPUT" + + - name: Set up documentation tools with pkgx + uses: pkgxdev/setup@4d4ae97af87ccb39ab8be4e073dea697fef2c6f7 # v5.0.0 + with: + +: | + doxygen.nl@${{ steps.tool-versions.outputs.doxygen }} + graphviz.org@${{ steps.tool-versions.outputs.graphviz }} + + - name: Build documentation + run: just docs + + - name: Publish documentation + uses: JamesIves/github-pages-deploy-action@d92aa235d04922e8f08b40ce78cc5442fcfbfa2f # v4.8.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} branch: gh-pages folder: docs/html - config_file: docs/Doxyfile diff --git a/Justfile b/Justfile index e110057c23..c425f1ba83 100644 --- a/Justfile +++ b/Justfile @@ -9,6 +9,10 @@ uv_version := "0.11.29" pinact_version := "4.1.0" pinact_module := "github.com/suzuki-shunsuke/pinact/v4/cmd/pinact@v" + pinact_version llvm_version := "22" +doxygen_version := "1.17.0" +graphviz_version := "15.1.0" +doxygen_windows_sha256 := "94594407c4cbca3049d76aacbb05d4a6f7d0f4e93c0de410b825d25ca5621c83" +graphviz_windows_sha256 := "c3ee71ff81ab97352082225574a140f20f5d6929d5f33d1097a1fe0e4161962a" zizmor_version := "1.26.1" primary_binary := if os_family() == "windows" { "out/build/reference/src/cdt.exe" } else { "out/build/reference/src/cdt" } rng_benchmark_binary := if os_family() == "windows" { "out/build/reference/tests/CDT_rng_benchmark.exe" } else { "out/build/reference/tests/CDT_rng_benchmark" } @@ -20,7 +24,7 @@ build: # Run fast, non-mutating local validation. [group('workflows')] -check: _justfile-check _format-check _yaml-check _action-lint _zizmor _whitespace-check _cmake-check release-check python-check semgrep semgrep-test +check: _justfile-check _format-check _yaml-check _action-lint _zizmor _whitespace-check _cmake-check docs-check release-check python-check semgrep semgrep-test @echo "Checks complete." # Run the comprehensive pre-commit/pre-push validation gate. @@ -28,6 +32,16 @@ check: _justfile-check _format-check _yaml-check _action-lint _zizmor _whitespac ci: check _pinact-check build @echo "CI validation complete." +# Validate the generated API documentation without modifying the worktree. +[group('workflows')] +docs-check: + ./scripts/doxygen.sh check "{{ doxygen_version }}" "{{ graphviz_version }}" + +# Generate the API documentation in docs/html for local inspection or publishing. +[group('workflows')] +docs: + ./scripts/doxygen.sh build "{{ doxygen_version }}" "{{ graphviz_version }}" + # Measure run-owned PCG sampling against the removed entropy-per-draw design. [group('workflows')] benchmark-rng draws='10000': build diff --git a/README.md b/README.md index d3583039ae..0035c22e8f 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ set, and the final release contract tracked by [issue #90](https://github.com/ac ## Table of contents -- [CDT-plusplus](#cdt-plusplus) +- [CDT-plusplus](README.md) - [Maintenance status](#maintenance-status) - [Introduction](#introduction) - [Regression-oracle scope](#regression-oracle-scope) @@ -158,10 +158,11 @@ The smallest pkgx-assisted host setup is: - pkgx - Just when invoking the recipes directly; `scripts/pkgx-build.sh` does not require it - Python 3.12 and uv when checking or running the Python support scripts +- Doxygen 1.17.0 and Graphviz 15.1.0 when checking or generating API documentation; pkgx can supply both -The pkgx launcher supplies Git, Bash, CMake, Ninja, Python, M4, Autoconf, Autoconf Archive, Automake, GNU -Libtool, Texinfo, and pkg-config. If pkgx is not installed, provide these tools conventionally through a package -manager such as [Homebrew] or apt: +The pkgx launcher supplies Git, Bash, CMake, Ninja, Python, Doxygen, Graphviz, M4, Autoconf, Autoconf Archive, +Automake, GNU Libtool, Texinfo, and pkg-config. If pkgx is not installed, provide these tools conventionally through +a package manager such as [Homebrew] or apt: - Git - Bash @@ -190,6 +191,8 @@ just sanitize asan # Build and exercise one Linux sanitizer preset just build # Bootstrap, configure, build, and smoke-test just run --help # Build as needed and run cdt with forwarded arguments just ci # Comprehensive pre-commit/pre-push validation +just docs-check # Validate Doxygen output without changing the worktree +just docs # Generate publishable documentation in docs/html just release-check # Validate release metadata and citation fields just update-actions # Update and repin Actions with pinact, then validate just python-sync # Install the locked Python development environment @@ -197,17 +200,18 @@ just python-check # Check Python formatting, lint, and types just python-fix # Apply safe Ruff fixes and formatting ``` -`check` covers repository-wide C++ formatting, Python formatting/lint/type checks, release metadata and citation -fields, YAML, GitHub Actions syntax and security, whitespace, and CMake preset parsing. `ci` adds the pinact policy -check and the supported build/test contract. The GitHub Actions Ubuntu GCC, Ubuntu Clang, macOS AppleClang, and -Windows MSVC jobs all run the same `just ci` command. Windows continues to compile with native MSVC; the locked -Python environment supplies `clang-format` only as a source formatter. Install the developer tools with Homebrew, -use equivalent system packages, or let pkgx supply the Unix environment ephemerally; pkgx remains optional. For -example: +`check` covers repository-wide C++ formatting, Python formatting/lint/type checks, strict Doxygen generation, +release metadata and citation fields, YAML, GitHub Actions syntax and security, whitespace, and CMake preset parsing. +`ci` adds the pinact policy check and the supported build/test contract. The GitHub Actions Ubuntu GCC, Ubuntu Clang, +macOS AppleClang, and Windows MSVC jobs all run the same `just ci` command. Windows continues to compile with native +MSVC; the locked Python environment supplies `clang-format` only as a source formatter. Install the developer tools +with Homebrew, use equivalent system packages, or let pkgx supply the Unix environment ephemerally; pkgx remains +optional. For example: ```bash uv sync --locked --group dev -pkgx +just.systems@1.57.0 +git-scm.org +cmake.org +ninja-build.org +python.org +zizmor just check +pkgx +just.systems@1.57.0 +git-scm.org +cmake.org +ninja-build.org +python.org \ + +doxygen.nl@1.17.0 +graphviz.org@15.1.0 +zizmor just check ``` [pinact](https://github.com/suzuki-shunsuke/pinact) uses [`.pinact.yaml`](.pinact.yaml) to retain immutable action @@ -358,21 +362,17 @@ policy are recorded in [`docs/reproducibility.md`](docs/reproducibility.md). The repository-wide scientific bibliography is maintained in [`REFERENCES.md`](REFERENCES.md). -If you have [Doxygen] installed you can generate the same information -locally using the configuration file in `docs/Doxyfile` by simply typing at the top -level directory ([Doxygen] will recursively search): +Validate the generated API documentation without modifying the worktree: ```bash -doxygen ./docs/Doxyfile +just docs-check ``` -This will generate a `docs/html/` directory containing -documentation generated from CDT++ source files. `USE_MATHJAX` has been enabled -in [Doxyfile] so that the LaTeX formulae can be rendered in the HTML -documentation using [MathJax]. `HAVE_DOT` is set to **YES** which allows -various graphs to be autogenerated by [Doxygen] using [GraphViz]. -If you do not have GraphViz installed, set this option to **NO** -(along with `UML_LOOK`). +To generate the same publishable output used by the documentation workflow, run `just docs`; it writes `docs/html/` +only after strict generation succeeds. Both recipes require the pinned Doxygen and Graphviz versions and use pkgx +ephemerally when matching local tools are unavailable. `USE_MATHJAX` allows [MathJax] to render LaTeX formulae, and +`HAVE_DOT` enables [GraphViz] diagrams. `just check` and therefore `just ci` include the non-mutating docs check on +every supported platform. The documentation workflow publishes the `just docs` output to the `gh-pages` branch. ## Citing CDT++ @@ -473,7 +473,7 @@ Your code should pass Continuous Integration: - `just clang-tidy` to analyze C++ with the pinned LLVM 22 toolchain -- `just check` for fast, non-mutating source, YAML, workflow, and CMake validation +- `just check` for fast, non-mutating source, documentation, YAML, workflow, and CMake validation - `just ci` for the supported build and complete validation contract before pushing diff --git a/docs/Doxyfile b/docs/Doxyfile index c004f31353..846de27ce7 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -1,4 +1,4 @@ -# Doxyfile 1.8.16 +# Doxyfile 1.17.0 # This file describes the settings to be used by the documentation system # doxygen (www.doxygen.org) for a project. @@ -99,7 +99,6 @@ OUTPUT_LANGUAGE = English # Possible values are: None, LTR, RTL and Context. # The default value is: None. -OUTPUT_TEXT_DIRECTION = None # If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member # descriptions after the members that are listed in the file and class @@ -267,7 +266,6 @@ ALIASES = # A mapping has the form "name=value". For example adding "class=itcl::class" # will allow you to use the command class in the itcl::class meaning. -TCL_SUBST = # Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources # only. Doxygen will then generate output that is more tailored for C. For @@ -335,6 +333,11 @@ EXTENSION_MAPPING = MARKDOWN_SUPPORT = YES +# Match the heading identifiers generated by GitHub-flavored Markdown so the +# README table of contents resolves in both renderers. + +MARKDOWN_ID_STYLE = GITHUB + # When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up # to that level are automatically included in the table of contents, even if # they do not have an id attribute. @@ -762,7 +765,7 @@ CITE_BIB_FILES = # messages are off. # The default value is: NO. -QUIET = NO +QUIET = YES # The WARNINGS tag can be used to turn on/off the warning messages that are # generated to standard error (stderr) by doxygen. If WARNINGS is set to YES @@ -778,7 +781,7 @@ WARNINGS = YES # will automatically be disabled. # The default value is: YES. -WARN_IF_UNDOCUMENTED = YES +WARN_IF_UNDOCUMENTED = NO # If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for # potential errors in the documentation, such as not documenting some parameters @@ -797,11 +800,12 @@ WARN_IF_DOC_ERROR = YES WARN_NO_PARAMDOC = NO -# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when -# a warning is encountered. +# If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS then Doxygen completes the +# run, reports all warnings, and returns a non-zero status afterward. +# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT. # The default value is: NO. -WARN_AS_ERROR = NO +WARN_AS_ERROR = FAIL_ON_WARNINGS # The WARN_FORMAT tag determines the format of the warning messages that doxygen # can produce. The string should contain the $file, $line, and $text tags, which @@ -829,7 +833,11 @@ WARN_LOGFILE = # spaces. See also FILE_PATTERNS and EXTENSION_MAPPING # Note: If this tag is empty the current directory is searched. -INPUT = +INPUT = README.md \ + REFERENCES.md \ + docs \ + include \ + src # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses @@ -917,6 +925,7 @@ EXCLUDE = build \ cmake-build-debug \ cmake-build-release \ cmake-build-relwithdebinfo \ + docs/html \ external \ scripts @@ -1131,7 +1140,6 @@ ALPHABETICAL_INDEX = YES # Minimum value: 1, maximum value: 20, default value: 5. # This tag requires that the tag ALPHABETICAL_INDEX is set to YES. -COLS_IN_ALPHA_INDEX = 5 # In case all classes in a project start with a common prefix, all classes will # be put under the same header in the alphabetical index. The IGNORE_PREFIX tag @@ -1267,7 +1275,6 @@ HTML_COLORSTYLE_GAMMA = 80 # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. -HTML_TIMESTAMP = YES # If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML # documentation will contain a main index with vertical navigation menus that @@ -1559,7 +1566,6 @@ FORMULA_FONTSIZE = 10 # The default value is: YES. # This tag requires that the tag GENERATE_HTML is set to YES. -FORMULA_TRANSPARENT = YES # Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see # https://www.mathjax.org) which uses client side Javascript for the rendering @@ -1861,7 +1867,6 @@ LATEX_HIDE_INDICES = NO # The default value is: NO. # This tag requires that the tag GENERATE_LATEX is set to YES. -LATEX_SOURCE_CODE = YES # The LATEX_BIB_STYLE tag can be used to specify the style to use for the # bibliography, e.g. plainnat, or ieeetr. See @@ -1877,7 +1882,6 @@ LATEX_BIB_STYLE = ieeetr # The default value is: NO. # This tag requires that the tag GENERATE_LATEX is set to YES. -LATEX_TIMESTAMP = NO # The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute) # path from which the emoji images will be read. If a relative path is entered, @@ -1951,7 +1955,6 @@ RTF_EXTENSIONS_FILE = # The default value is: NO. # This tag requires that the tag GENERATE_RTF is set to YES. -RTF_SOURCE_CODE = NO #--------------------------------------------------------------------------- # Configuration options related to the man page output @@ -2056,7 +2059,6 @@ DOCBOOK_OUTPUT = docbook # The default value is: NO. # This tag requires that the tag GENERATE_DOCBOOK is set to YES. -DOCBOOK_PROGRAMLISTING = NO #--------------------------------------------------------------------------- # Configuration options for the AutoGen Definitions output @@ -2243,7 +2245,6 @@ EXTERNAL_PAGES = YES # powerful graphs. # The default value is: YES. -CLASS_DIAGRAMS = YES # You can include diagrams made with dia in doxygen documentation. Doxygen will # then run dia to produce the diagram and insert it in the documentation. The @@ -2285,14 +2286,12 @@ DOT_NUM_THREADS = 0 # The default value is: Helvetica. # This tag requires that the tag HAVE_DOT is set to YES. -DOT_FONTNAME = Helvetica # The DOT_FONTSIZE tag can be used to set the size (in points) of the font of # dot graphs. # Minimum value: 4, maximum value: 24, default value: 10. # This tag requires that the tag HAVE_DOT is set to YES. -DOT_FONTSIZE = 10 # By default doxygen will tell dot to use the default font as specified with # DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set @@ -2516,7 +2515,6 @@ MAX_DOT_GRAPH_DEPTH = 0 # The default value is: NO. # This tag requires that the tag HAVE_DOT is set to YES. -DOT_TRANSPARENT = NO # Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output # files in one run (i.e. multiple -o and -T options on the command line). This @@ -2525,7 +2523,6 @@ DOT_TRANSPARENT = NO # The default value is: NO. # This tag requires that the tag HAVE_DOT is set to YES. -DOT_MULTI_TARGETS = NO # If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page # explaining the meaning of the various boxes and arrows in the dot generated diff --git a/include/Ergodic_moves_3.hpp b/include/Ergodic_moves_3.hpp index 986756977f..771057c99d 100644 --- a/include/Ergodic_moves_3.hpp +++ b/include/Ergodic_moves_3.hpp @@ -86,7 +86,7 @@ namespace ergodic_moves { return t_manifold; } // null_move /// @brief Perform a TriangulationDataStructure_3::flip on a facet - /// @param t_manifold The manifold containing the cell to flip + /// @param triangulation The triangulation containing the cell to flip /// @param to_be_moved The cell on which to try the move /// @returns True if move succeeded /// @see @@ -133,7 +133,10 @@ namespace ergodic_moves /// /// If successful, the triangulation is no longer Delaunay. /// + /// @tparam Generator A uniform random bit generator type /// @param t_manifold The simplicial manifold + /// @param generator Caller-owned generator whose state advances during the + /// move /// @returns The Expected (2,3) moved manifold or an Unexpected template [[nodiscard]] inline auto do_23_move(Manifold const& t_manifold, @@ -178,7 +181,7 @@ namespace ergodic_moves } /// @brief Perform a TriangulationDataStructure_3::flip on an edge - /// @param t_manifold The manifold containing the edge to flip + /// @param triangulation The triangulation containing the edge to flip /// @param to_be_moved The edge on which to try the move /// @returns True if move succeeded /// @see @@ -196,7 +199,10 @@ namespace ergodic_moves /// This function calls try_32_move on timelike edges drawn from a /// randomly shuffled container until it succeeds or runs out of edges. /// If successful, the triangulation is no longer Delaunay. + /// @tparam Generator A uniform random bit generator type /// @param t_manifold The simplicial manifold + /// @param generator Caller-owned generator whose state advances during the + /// move /// @returns The Expected (3,2) moved manifold or an Unexpected template [[nodiscard]] inline auto do_32_move(Manifold const& t_manifold, @@ -269,7 +275,11 @@ namespace ergodic_moves /// If successful, the triangulation is no longer Delaunay. /// @image html 26.png /// @image latex 26.eps width=7cm + /// @tparam Generator A uniform random bit generator type /// @param t_manifold The simplicial manifold + /// @param generator Caller-owned generator whose state advances during the + /// move + /// @param only_first_site Whether to examine only one uniformly selected site /// @returns The Expected (2,6) moved manifold or an Unexpected template [[nodiscard]] inline auto do_26_move_impl(Manifold const& t_manifold, @@ -548,7 +558,10 @@ namespace ergodic_moves /// If successful, the triangulation remains Delaunay. (Other moves may /// change this, however.) /// + /// @tparam Generator A uniform random bit generator type /// @param t_manifold The simplicial manifold + /// @param generator Caller-owned generator whose state advances during the + /// move /// @returns The Expected (6,2) moved manifold or Unexpected template [[nodiscard]] inline auto do_62_move(Manifold const& t_manifold, @@ -848,7 +861,10 @@ namespace ergodic_moves /// move is not required to preserve the Euclidean Delaunay property of the /// coordinates used to represent the abstract triangulation. /// + /// @tparam Generator A uniform random bit generator type /// @param t_manifold The simplicial manifold + /// @param generator Caller-owned generator whose state advances during the + /// move /// @return The Expected (4,4) moved manifold or Unexpected template [[nodiscard]] inline auto do_44_move(Manifold const& t_manifold, diff --git a/include/Foliated_triangulation.hpp b/include/Foliated_triangulation.hpp index 5103e1fa1a..5fa0290e7a 100644 --- a/include/Foliated_triangulation.hpp +++ b/include/Foliated_triangulation.hpp @@ -65,7 +65,7 @@ using Spherical_points_generator_t = typename TriangulationTraits::Spherical_points_generator; /// @concept ContainerType -/// @brief This is std::movable from +/// @brief Requires `std::movable`, defined in the standard `` header. /// @details Right now the real restriction on Containers is that elements must /// be swappable in order for std::shuffle to work. template @@ -900,11 +900,14 @@ namespace foliated_triangulations /// @details Makes a solid ball of successive layers of spheres at /// a given radius. /// @tparam dimension The dimensionality of the simplices + /// @tparam Generator Uniform random bit generator type owned by the caller /// @param t_simplices The desired number of simplices in the triangulation /// @param t_timeslices The desired number of timeslices in the /// triangulation /// @param initial_radius The radius of the first time slice /// @param foliation_spacing The distance between successive time slices + /// @param generator Caller-owned random stream whose state is maintained by + /// the caller and advanced during this call /// @return A container of (vertex, timevalue) pairs template [[nodiscard]] auto make_foliated_ball(Int_precision const t_simplices, @@ -984,10 +987,13 @@ namespace foliated_triangulations /// @brief Make a Delaunay triangulation /// @tparam dimension Dimensionality of the Delaunay triangulation + /// @tparam Generator Uniform random bit generator type owned by the caller /// @param t_simplices Number of desired simplices /// @param t_timeslices Number of desired timeslices /// @param initial_radius Radius of first timeslice /// @param foliation_spacing Radial separation between timeslices + /// @param generator Caller-owned random stream whose state is maintained by + /// the caller and advanced during this call /// @return A Delaunay triangulation with a timevalue for each vertex /// @see [CGAL triangulations](../REFERENCES.md#cgal-triangulations) template @@ -1246,6 +1252,12 @@ namespace foliated_triangulations {} /// @brief Constructor with a caller-owned initialization stream. + /// @param t_simplices Desired number of simplices + /// @param t_timeslices Desired number of timeslices + /// @param generator Caller-owned initialization stream whose state is + /// advanced during construction + /// @param t_initial_radius Radius of the first timeslice + /// @param t_foliation_spacing Radial separation between timeslices FoliatedTriangulation(Int_precision const t_simplices, Int_precision const t_timeslices, cdt::Random& generator, @@ -1258,6 +1270,12 @@ namespace foliated_triangulations {} /// @brief Construct from an explicit temporary initialization stream. + /// @param t_simplices Desired number of simplices + /// @param t_timeslices Desired number of timeslices + /// @param generator Temporary initialization stream whose state is consumed + /// during construction + /// @param t_initial_radius Radius of the first timeslice + /// @param t_foliation_spacing Radial separation between timeslices FoliatedTriangulation(Int_precision const t_simplices, Int_precision const t_timeslices, cdt::Random&& generator, diff --git a/include/Formatters.hpp b/include/Formatters.hpp index ac34fcbb0f..c4ed912f62 100644 --- a/include/Formatters.hpp +++ b/include/Formatters.hpp @@ -19,23 +19,26 @@ #include "Triangulation_traits.hpp" -// Formatter specialization for CGAL::Point_3 -template -struct fmt::formatter> +namespace fmt { - // Format specification handling - keeping it simple for now - auto constexpr parse(format_parse_context& ctx) -> decltype(ctx.begin()) - { return ctx.begin(); } - - // Format the point as a string with coordinates - template - auto format(CGAL::Point_3 const& point, FormatContext& ctx) const - -> decltype(ctx.out()) + /// @brief Formatter specialization for `CGAL::Point_3`. + template + struct formatter> { - std::stringstream ss; - ss << point; - return fmt::format_to(ctx.out(), "{}", ss.str()); - } -}; + // Format specification handling - keeping it simple for now + auto constexpr parse(format_parse_context& ctx) -> decltype(ctx.begin()) + { return ctx.begin(); } + + // Format the point as a string with coordinates + template + auto format(CGAL::Point_3 const& point, FormatContext& ctx) const + -> decltype(ctx.out()) + { + std::stringstream ss; + ss << point; + return fmt::format_to(ctx.out(), "{}", ss.str()); + } + }; +} // namespace fmt #endif // CDT_PLUSPLUS_FORMATTERS_HPP diff --git a/include/Manifold.hpp b/include/Manifold.hpp index 0882619f7d..62a118ced2 100644 --- a/include/Manifold.hpp +++ b/include/Manifold.hpp @@ -109,6 +109,12 @@ namespace manifolds {} /// @brief Construct a manifold with a caller-owned initialization stream. + /// @param t_desired_simplices Desired number of simplices + /// @param t_desired_timeslices Desired number of timeslices + /// @param generator Caller-owned initialization stream whose state is + /// advanced during construction + /// @param t_initial_radius Radius of the first timeslice + /// @param t_foliation_spacing Radial separation between timeslices Manifold(Int_precision const t_desired_simplices, Int_precision const t_desired_timeslices, cdt::Random& generator, double const t_initial_radius = INITIAL_RADIUS, @@ -120,6 +126,12 @@ namespace manifolds {} /// @brief Construct from an explicit temporary initialization stream. + /// @param t_desired_simplices Desired number of simplices + /// @param t_desired_timeslices Desired number of timeslices + /// @param generator Temporary initialization stream whose state is consumed + /// during construction + /// @param t_initial_radius Radius of the first timeslice + /// @param t_foliation_spacing Radial separation between timeslices Manifold(Int_precision const t_desired_simplices, Int_precision const t_desired_timeslices, cdt::Random&& generator, double const t_initial_radius = INITIAL_RADIUS, diff --git a/include/Utilities.hpp b/include/Utilities.hpp index f39ca2ee46..76d07596ce 100644 --- a/include/Utilities.hpp +++ b/include/Utilities.hpp @@ -384,6 +384,9 @@ namespace utilities /// /// @tparam NumberType The type of number in the RNG /// @tparam Distribution The distribution type, usually uniform + /// @tparam Generator A uniform random bit generator type + /// @param generator Caller-owned generator whose state advances during + /// sampling /// @param t_min_value The minimum value /// @param t_max_value The maximum value /// @returns A random value in the distribution between min_value and diff --git a/scripts/doxygen.sh b/scripts/doxygen.sh new file mode 100755 index 0000000000..179d7063b9 --- /dev/null +++ b/scripts/doxygen.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + echo "Usage: $0 " >&2 + exit 2 +} + +[[ $# -eq 3 ]] || usage + +mode="$1" +doxygen_version="$2" +graphviz_version="$3" +[[ "$mode" == "check" || "$mode" == "build" ]] || usage + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$repo_root" + +doxygen_matches() { + command -v doxygen >/dev/null 2>&1 && + [[ "$(doxygen --version)" == "$doxygen_version" ]] +} + +graphviz_matches() { + local installed_version + command -v dot >/dev/null 2>&1 || return 1 + installed_version="$(dot -V 2>&1 | sed -nE 's/^dot - graphviz version ([^ ]+).*/\1/p')" + [[ "$installed_version" == "$graphviz_version" ]] +} + +if ! doxygen_matches || ! graphviz_matches; then + if command -v pkgx >/dev/null 2>&1 && [[ -z "${CDT_DOXYGEN_TOOLCHAIN_ACTIVE:-}" ]]; then + export CDT_DOXYGEN_TOOLCHAIN_ACTIVE=1 + exec pkgx "+doxygen.nl@$doxygen_version" "+graphviz.org@$graphviz_version" -- \ + "$0" "$mode" "$doxygen_version" "$graphviz_version" + fi + + echo "Doxygen $doxygen_version and Graphviz $graphviz_version are required." >&2 + echo "Install those versions or install pkgx so the repository can provide them ephemerally." >&2 + exit 1 +fi + +temporary_output="$(mktemp -d "${TMPDIR:-/tmp}/cdt-doxygen.XXXXXX")" +cleanup() { + rm -rf "$temporary_output" +} +trap cleanup EXIT + +config_output="$temporary_output" +if command -v cygpath >/dev/null 2>&1; then + config_output="$(cygpath -m "$temporary_output")" +fi + +{ + cat docs/Doxyfile + printf '\nOUTPUT_DIRECTORY = "%s"\n' "$config_output" +} | doxygen - + +generated_html="$temporary_output/html" +if [[ ! -f "$generated_html/index.html" ]]; then + echo "Doxygen completed without generating html/index.html." >&2 + exit 1 +fi + +if [[ "$mode" == "build" ]]; then + published_html="$repo_root/docs/html" + rm -rf "$published_html" + mv "$generated_html" "$published_html" + touch "$published_html/.nojekyll" + echo "Documentation generated in docs/html/." +else + echo "Documentation validation complete." +fi diff --git a/scripts/optimize_initialize.py b/scripts/optimize_initialize.py index df982cb7a6..3f429178ef 100644 --- a/scripts/optimize_initialize.py +++ b/scripts/optimize_initialize.py @@ -55,18 +55,17 @@ def _initializer_binary(repository_root: Path, platform: str = sys.platform) -> def _run_experiments(initialize_binary: Path, api_key: str) -> None: """Run the historical Comet parameter sweep.""" - import comet_ml as cm # noqa: PLC0415 import matplotlib.pyplot as plt # noqa: PLC0415 import numpy as np # noqa: PLC0415 from comet_ml import Experiment # noqa: PLC0415 parameters = [(initial_radius, spacing) for initial_radius in range(1, 4) for spacing in np.arange(1, 2.5, 0.5)] - try: - for parameter_pair in parameters: - experiment = Experiment(api_key=api_key, project_name="cdt-plusplus") + for parameter_pair in parameters: + experiment = Experiment(api_key=api_key, project_name="cdt-plusplus") + try: hyper_params = {"simplices": 12000, "foliations": 12} - experiment.log_multiple_params(hyper_params) + experiment.log_parameters(hyper_params) init_radius = parameter_pair[0] radial_factor = parameter_pair[1] @@ -115,9 +114,8 @@ def _run_experiments(initialize_binary: Path, api_key: str) -> None: plt.grid(visible=True) experiment.log_figure(figure_name="Volume per Timeslice", figure=plt) plt.clf() + finally: experiment.end() - except cm.exceptions.NoMoreSuggestionsAvailable: - print("No more suggestions.") def main(argv: Sequence[str] | None = None) -> int: diff --git a/scripts/sanitizer.sh b/scripts/sanitizer.sh index cb21288b71..5274a3ab47 100755 --- a/scripts/sanitizer.sh +++ b/scripts/sanitizer.sh @@ -36,5 +36,5 @@ ctest --test-dir "${build_dir}" --label-exclude full-suite-duplicate --no-tests= mkdir -p -- "${run_dir}" cd -- "${run_dir}" -"${build_dir}/src/initialize" -s -n32000 -t11 -o +"${build_dir}/src/initialize" -s -n32000 -t11 --seed 92 "${build_dir}/src/cdt" -s -n64 -t3 -a.6 -k1.1 -l.1 -p10 --no-output diff --git a/scripts/tests/test_optimize_initialize.py b/scripts/tests/test_optimize_initialize.py index 582cc272a6..4dc9afcac4 100644 --- a/scripts/tests/test_optimize_initialize.py +++ b/scripts/tests/test_optimize_initialize.py @@ -33,6 +33,18 @@ def test_initializer_output_is_parsed(self) -> None: Final number of simplices: 92""" self.assertEqual(_parse_initializer_output(output), (92, [(1, 12), (2, 24)])) + def test_initializer_output_requires_final_simplex_count(self) -> None: + """The sweep rejects output without the final triangulation size.""" + output = "Timeslice 1 has 12 spacelike faces." + with self.assertRaisesRegex(RuntimeError, "did not report the final number of simplices"): + _parse_initializer_output(output) + + def test_initializer_output_requires_volume_profile(self) -> None: + """The sweep rejects output without any timeslice volumes.""" + output = "Final number of simplices: 92" + with self.assertRaisesRegex(RuntimeError, "did not contain a timeslice volume profile"): + _parse_initializer_output(output) + if __name__ == "__main__": unittest.main() diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 19354ed677..d9f64b528a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -53,4 +53,10 @@ target_link_libraries( # Run unit tests add_test(NAME cdt-unit-tests COMMAND $) -set_tests_properties(cdt-unit-tests PROPERTIES LABELS "unit" TIMEOUT 180) +set(cdt_unit_test_timeout 180) +if(ENABLE_SANITIZER_ADDRESS) + # CGAL-heavy unit tests routinely need 12-19 minutes under ASan on hosted runners. + set(cdt_unit_test_timeout 1800) +endif() +set_tests_properties( + cdt-unit-tests PROPERTIES LABELS "unit" TIMEOUT "${cdt_unit_test_timeout}") diff --git a/tests/Ergodic_moves_3_test.cpp b/tests/Ergodic_moves_3_test.cpp index e6932a2002..81660e9b48 100644 --- a/tests/Ergodic_moves_3_test.cpp +++ b/tests/Ergodic_moves_3_test.cpp @@ -578,6 +578,7 @@ SCENARIO("Rejected topology moves preserve the source value" * WHEN("A (2,6) move is proposed") { + CAPTURE(random.seed()); auto const result = ergodic_moves::do_26_move(source, random); THEN("The move is rejected without changing the source") diff --git a/tests/Random_benchmark.cpp b/tests/Random_benchmark.cpp index ecc7a23fdc..b940601fd7 100644 --- a/tests/Random_benchmark.cpp +++ b/tests/Random_benchmark.cpp @@ -64,9 +64,14 @@ try return move_tracker::generate_random_move_3(per_draw_random); }); - auto const owned_ns = static_cast(owned_time.count()); - auto const speedup = - static_cast(entropy_time.count()) / owned_ns; + auto const owned_ns = owned_time.count(); + if (owned_ns == 0) + { + throw std::runtime_error{ + "owned-stream duration is below clock resolution; increase draw count"}; + } + auto const speedup = static_cast(entropy_time.count()) / + static_cast(owned_ns); std::cout << "draws=" << draws << '\n' << "before_entropy_per_draw_ns=" << entropy_time.count() << '\n' << "after_run_owned_pcg_ns=" << owned_time.count() << '\n' diff --git a/tests/S3Action_test.cpp b/tests/S3Action_test.cpp index 0a9586650e..bc39dd9306 100644 --- a/tests/S3Action_test.cpp +++ b/tests/S3Action_test.cpp @@ -168,6 +168,10 @@ SCENARIO("Bulk action precision survives the acceptance boundary" * CHECK_EQ(current.get_precision(), mpfr_values::precision); CHECK_EQ(proposed.get_precision(), mpfr_values::precision); CHECK(mpfr_zero_p(delta.fr()) == 0); + } + AND_THEN( + "Downcasting to double collapses the distinction that MPFR preserves.") + { CHECK_EQ(mpfr_values::to_double(current), mpfr_values::to_double(proposed)); } From 666b1b929ed624249d621e490f782c7d27693c63 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Mon, 20 Jul 2026 16:19:35 -0700 Subject: [PATCH 03/13] fix(tooling): isolate CodeQL and seed support workflows - prepare dependencies before CodeQL and trace only CDT++ production targets - make sanitizer and initializer optimization runs replayable with recorded seeds - reject malformed initializer profiles instead of silently ignoring them - clarify cross-platform build entry points and generator-agnostic RNG documentation --- .github/workflows/codeql.yml | 14 ++++- Justfile | 19 ++++++ README.md | 40 +++++++----- include/Utilities.hpp | 6 +- scripts/codeql-build.sh | 41 ++++++++++++ scripts/optimize_initialize.py | 76 +++++++++++++++++------ scripts/pkgx-build.sh | 11 ++++ scripts/sanitizer.sh | 2 +- scripts/tests/test_optimize_initialize.py | 32 +++++++++- 9 files changed, 203 insertions(+), 38 deletions(-) create mode 100755 scripts/codeql-build.sh diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 87e3cd2bc9..0d8763cef5 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -64,6 +64,10 @@ jobs: with: persist-credentials: false + - name: Set up Just + if: matrix.language == 'c-cpp' + uses: ./.github/actions/setup-just + - name: Set up C++ environment with pkgx if: matrix.language == 'c-cpp' uses: pkgxdev/setup@4d4ae97af87ccb39ab8be4e073dea697fef2c6f7 # v5.0.0 @@ -72,6 +76,14 @@ jobs: if: matrix.language == 'c-cpp' uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11.6 + - name: Prepare C++ dependencies outside CodeQL tracing + if: matrix.language == 'c-cpp' + env: + CC: gcc + CDT_PKGX_COMPILER_PACKAGE: gnu.org/gcc@16 + CXX: g++ + run: just codeql-prepare + - name: Initialize CodeQL uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 with: @@ -84,7 +96,7 @@ jobs: CC: gcc CDT_PKGX_COMPILER_PACKAGE: gnu.org/gcc@16 CXX: g++ - run: ./scripts/pkgx-build.sh + run: just codeql-build - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 diff --git a/Justfile b/Justfile index c425f1ba83..177de27ef8 100644 --- a/Justfile +++ b/Justfile @@ -32,6 +32,16 @@ check: _justfile-check _format-check _yaml-check _action-lint _zizmor _whitespac ci: check _pinact-check build @echo "CI validation complete." +# Configure dependencies before CodeQL begins tracing the C++ build. +[group('workflows')] +codeql-prepare: + just _codeql-phase prepare + +# Compile only project-owned production targets for CodeQL extraction. +[group('workflows')] +codeql-build: + just _codeql-phase build + # Validate the generated API documentation without modifying the worktree. [group('workflows')] docs-check: @@ -187,6 +197,15 @@ _build-unix: fi exec ./scripts/build.sh +[private] +_codeql-phase phase: + #!/usr/bin/env bash + set -euo pipefail + if command -v pkgx >/dev/null; then + exec ./scripts/pkgx-build.sh --codeql {{ phase }} + fi + exec ./scripts/codeql-build.sh {{ phase }} + [private] _cmake-check: #!/usr/bin/env bash diff --git a/README.md b/README.md index 0035c22e8f..b0c46977ee 100644 --- a/README.md +++ b/README.md @@ -139,10 +139,11 @@ Windows development. ### Current reference-suite status -With the pinned baseline, the reference configuration and build succeed on macOS with AppleClang. `build.sh` runs all -21 CTest entries on every supported platform: one unit-test launcher containing 83 doctest scenarios and 20 CLI -integration tests. The same `reference-smoke` preset is the supported local and CI contract; there are no overlapping -focused registrations that can pass while omitting another doctest suite. +With the pinned baseline, the reference configuration and build succeed on macOS with AppleClang. The cross-platform +`just build` command runs all 21 CTest entries through `scripts/build.sh` on Unix and `scripts/build.bat` on Windows: +one unit-test launcher containing 83 doctest scenarios and 20 CLI integration tests. The same `reference-smoke` preset +is the supported local and CI contract; there are no overlapping focused registrations that can pass while omitting +another doctest suite. ## Setup @@ -185,6 +186,8 @@ The repository-root [Justfile](Justfile) provides the same small command vocabul ```bash just check # Fast, non-mutating local checks +just codeql-prepare # Configure dependencies before CodeQL tracing +just codeql-build # Build production targets for CodeQL extraction just fix # Format C++/Python source and the Justfile just clang-tidy # Analyze C++ with LLVM 22 just sanitize asan # Build and exercise one Linux sanitizer preset @@ -238,13 +241,19 @@ export VCPKG_ROOT="$PWD/.cache/vcpkg" CI uses `lukka/run-vcpkg`, which derives the vcpkg checkout commit from the same manifest baseline and supplies a binary cache. No separately maintained repository variable is required. +CodeQL keeps third-party implementation findings out of CDT++ results through a two-phase manual build. +`just codeql-prepare` configures the project, installs manifest dependencies before CodeQL starts tracing, and uses a +build directory under the host temporary directory so installed headers are outside the checkout. After CodeQL +initialization, `just codeql-build` compiles only the `cdt` and `initialize` production targets with tests disabled. +The regular `just build` and `just ci` contracts continue to build and run the complete test suite. + ## Build -Run `just build` from the repository root. It delegates to `./scripts/build.sh`, which can itself be run from any -working directory for troubleshooting. If `VCPKG_ROOT` already names the clean official checkout at the manifest -baseline, the script respects it; otherwise it uses the pinned disposable checkout described above. The script -invokes the `reference` configure and build presets followed by the `reference-smoke` test preset; products and tests -are isolated under `out/build/reference`. Windows uses the same presets through `scripts\build.bat`, while +Run `just build` from the repository root. It delegates to `./scripts/build.sh` on Unix and `scripts\build.bat` on +Windows; either platform-specific script can itself be run from any working directory for troubleshooting. If +`VCPKG_ROOT` already names the clean official checkout at the manifest baseline, the script respects it; otherwise it +uses the pinned disposable checkout described above. Both scripts invoke the `reference` configure and build presets +followed by the `reference-smoke` test preset; products and tests are isolated under `out/build/reference`, while `scripts\fast-build.bat` configures the same reference tree and builds only the primary `cdt` target. All entry points preserve a compatible CMake cache and refresh it only when the selected vcpkg toolchain path changes. @@ -385,10 +394,11 @@ subsequent releases. ## Testing -Run `just build`; it delegates to `./scripts/build.sh`, builds the test target, and executes all 21 CTest entries: one -unit-test launcher containing 83 doctest scenarios plus 20 executable integration tests covering normal CLI use and -invalid-boundary rejection. CTest labels the launcher `unit` and every process-level test `integration`; invalid-input -tests also carry the `cli-boundary` subcategory. Run `just ci` for the complete local validation gate. +Run `just build`; it selects `scripts/build.sh` on Unix or `scripts\build.bat` on Windows, builds the test target, and +executes all 21 CTest entries: one unit-test launcher containing 83 doctest scenarios plus 20 executable integration +tests covering normal CLI use and invalid-boundary rejection. CTest labels the launcher `unit` and every process-level +test `integration`; invalid-input tests also carry the `cli-boundary` subcategory. Run `just ci` for the complete local +validation gate. `just check` also runs the repository-owned Semgrep policy and its annotated fixtures. Use `just semgrep-test` while changing the rules and `just semgrep` to @@ -452,7 +462,9 @@ uv run --locked --group experiments cdt-mnist-experiment ``` Run these commands from the repository root. Set `COMET_API_KEY` before starting the parameter optimization; use -`--repository-root` when invoking it from another directory. The experiment results are then available in Comet. +`--repository-root` when invoking it from another directory. The optimizer uses seed `92` by default for every +parameter pair so results can be compared and replayed; pass `--seed SEED` to select and record another root seed. The +experiment results are then available in Comet. Migration of these legacy scripts to Python 3.14, PyTorch, and the current Comet API is tracked by [#104](https://github.com/acgetchell/CDT-plusplus/issues/104). diff --git a/include/Utilities.hpp b/include/Utilities.hpp index 76d07596ce..dfdc9745ad 100644 --- a/include/Utilities.hpp +++ b/include/Utilities.hpp @@ -375,10 +375,10 @@ namespace utilities return roll; } // die_roll() - /// @brief Generate random numbers + /// @brief Generate random numbers with a caller-supplied generator /// - /// Uses Melissa E. O'Neill's Permuted Congruential Generator for high-quality - /// RNG which passes the TestU01 statistical tests. + /// Accepts any uniform random bit generator. When callers provide + /// `cdt::Random`, sampling uses its run-owned PCG engine. /// @see [PCG random-number /// generators](../REFERENCES.md#pcg-random-number-generators) /// diff --git a/scripts/codeql-build.sh b/scripts/codeql-build.sh new file mode 100755 index 0000000000..694f8bd4b4 --- /dev/null +++ b/scripts/codeql-build.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd -- "${script_dir}/.." && pwd)" +temporary_root="${RUNNER_TEMP:-${TMPDIR:-/tmp}}" +codeql_build_dir="${CDT_CODEQL_BUILD_DIR:-${temporary_root%/}/cdt-plusplus-codeql-build}" +phase="${1:-}" + +if [[ "${phase}" != "prepare" && "${phase}" != "build" ]]; then + printf 'Usage: %s prepare|build\n' "$0" >&2 + exit 2 +fi + +source "${script_dir}/prepare-vcpkg.sh" +prepare_reference_environment +prepare_vcpkg "${repo_root}" +prepare_cmake_cache "${codeql_build_dir}" + +case "${phase}" in + prepare) + # Configure before CodeQL starts tracing. Manifest installation therefore + # builds third-party sources outside the CodeQL database, while the build + # directory keeps installed dependency headers outside the checkout. + cmake -S "${repo_root}" -B "${codeql_build_dir}" -G Ninja \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DCMAKE_CXX_EXTENSIONS=OFF \ + -DCMAKE_CXX_STANDARD=23 \ + -DCMAKE_CXX_STANDARD_REQUIRED=ON \ + -DENABLE_CACHE=OFF \ + -DENABLE_TESTING=OFF + ;; + build) + if [[ ! -f "${codeql_build_dir}/CMakeCache.txt" ]]; then + printf 'CodeQL build is not configured; run `just codeql-prepare` before CodeQL initialization.\n' >&2 + exit 1 + fi + cmake --build "${codeql_build_dir}" --parallel 2 --target cdt initialize + ;; +esac diff --git a/scripts/optimize_initialize.py b/scripts/optimize_initialize.py index 3f429178ef..8d4f62a4a8 100644 --- a/scripts/optimize_initialize.py +++ b/scripts/optimize_initialize.py @@ -11,7 +11,22 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from collections.abc import Sequence + from collections.abc import Mapping, Sequence + +MAX_RANDOM_SEED = (1 << 64) - 1 + + +def _parse_seed(value: str) -> int: + """Parse one unsigned 64-bit seed before creating online experiments.""" + try: + seed = int(value, 10) + except ValueError as error: + message = "seed must be an unsigned 64-bit integer" + raise argparse.ArgumentTypeError(message) from error + if seed < 0 or seed > MAX_RANDOM_SEED: + message = "seed must be between 0 and 18446744073709551615" + raise argparse.ArgumentTypeError(message) + return seed def _parse_args(argv: Sequence[str]) -> argparse.Namespace: @@ -23,6 +38,12 @@ def _parse_args(argv: Sequence[str]) -> argparse.Namespace: default=Path.cwd(), help="CDT++ checkout containing out/build/reference (default: current directory)", ) + parser.add_argument( + "--seed", + type=_parse_seed, + default=92, + help="root initializer seed used for every parameter pair (default: 92)", + ) return parser.parse_args(argv) @@ -35,8 +56,10 @@ def _parse_initializer_output(output: str) -> tuple[int, list[tuple[int, int]]]: final_simplices = int(match.group("count")) elif line.startswith("Timeslice"): match = re.fullmatch(r"Timeslice (?P\d+) has (?P\d+) spacelike faces[.]", line) - if match: - graph.append((int(match.group("timeslice")), int(match.group("volume")))) + if match is None: + message = f"Initializer output contained a malformed timeslice volume: {line!r}" + raise RuntimeError(message) + graph.append((int(match.group("timeslice")), int(match.group("volume")))) if final_simplices is None: message = "Initializer output did not report the final number of simplices." @@ -53,7 +76,30 @@ def _initializer_binary(repository_root: Path, platform: str = sys.platform) -> return repository_root / "out" / "build" / "reference" / "src" / executable -def _run_experiments(initialize_binary: Path, api_key: str) -> None: +def _initializer_command( + initialize_binary: Path, + hyper_params: Mapping[str, int], + initial_radius: int, + radial_factor: float, +) -> list[str]: + """Build one replayable initializer invocation.""" + return [ + str(initialize_binary), + "-s", + "-n", + str(hyper_params["simplices"]), + "-t", + str(hyper_params["foliations"]), + "-i", + str(initial_radius), + "-f", + str(radial_factor), + "--seed", + str(hyper_params["seed"]), + ] + + +def _run_experiments(initialize_binary: Path, api_key: str, seed: int) -> None: """Run the historical Comet parameter sweep.""" import matplotlib.pyplot as plt # noqa: PLC0415 import numpy as np # noqa: PLC0415 @@ -64,23 +110,17 @@ def _run_experiments(initialize_binary: Path, api_key: str) -> None: for parameter_pair in parameters: experiment = Experiment(api_key=api_key, project_name="cdt-plusplus") try: - hyper_params = {"simplices": 12000, "foliations": 12} + hyper_params = {"simplices": 12000, "foliations": 12, "seed": seed} experiment.log_parameters(hyper_params) init_radius = parameter_pair[0] radial_factor = parameter_pair[1] - command = [ - str(initialize_binary), - "-s", - "-n", - str(hyper_params["simplices"]), - "-t", - str(hyper_params["foliations"]), - "-i", - str(init_radius), - "-f", - str(radial_factor), - ] + command = _initializer_command( + initialize_binary, + hyper_params, + initial_radius=init_radius, + radial_factor=radial_factor, + ) print(command) # The executable and numeric parameters are repository-controlled. @@ -133,7 +173,7 @@ def main(argv: Sequence[str] | None = None) -> int: return 2 try: - _run_experiments(initialize_binary, api_key) + _run_experiments(initialize_binary, api_key, args.seed) except ModuleNotFoundError as error: print( f"Missing experiment dependency {error.name!r}; run with `uv run --group experiments cdt-optimize-initialize`.", diff --git a/scripts/pkgx-build.sh b/scripts/pkgx-build.sh index 8773cbb367..fab4ecf696 100755 --- a/scripts/pkgx-build.sh +++ b/scripts/pkgx-build.sh @@ -25,4 +25,15 @@ if [[ -n "${CDT_PKGX_COMPILER_PACKAGE:-}" ]]; then fi cd -- "${repo_root}" + +if [[ "${1:-}" == "--codeql" ]]; then + shift + exec pkgx "${pkgx_tools[@]}" -- "${script_dir}/codeql-build.sh" "$@" +fi + +if [[ "$#" -ne 0 ]]; then + printf 'Usage: %s [--codeql prepare|build]\n' "$0" >&2 + exit 2 +fi + exec pkgx "${pkgx_tools[@]}" -- "${script_dir}/build.sh" diff --git a/scripts/sanitizer.sh b/scripts/sanitizer.sh index 5274a3ab47..951f83931c 100755 --- a/scripts/sanitizer.sh +++ b/scripts/sanitizer.sh @@ -37,4 +37,4 @@ ctest --test-dir "${build_dir}" --label-exclude full-suite-duplicate --no-tests= mkdir -p -- "${run_dir}" cd -- "${run_dir}" "${build_dir}/src/initialize" -s -n32000 -t11 --seed 92 -"${build_dir}/src/cdt" -s -n64 -t3 -a.6 -k1.1 -l.1 -p10 --no-output +"${build_dir}/src/cdt" -s -n64 -t3 -a.6 -k1.1 -l.1 -p10 --no-output --seed 92 diff --git a/scripts/tests/test_optimize_initialize.py b/scripts/tests/test_optimize_initialize.py index 4dc9afcac4..0d3f456cbb 100644 --- a/scripts/tests/test_optimize_initialize.py +++ b/scripts/tests/test_optimize_initialize.py @@ -2,10 +2,11 @@ from __future__ import annotations +import argparse import unittest from pathlib import Path -from scripts.optimize_initialize import _initializer_binary, _parse_initializer_output +from scripts.optimize_initialize import _initializer_binary, _initializer_command, _parse_args, _parse_initializer_output, _parse_seed class OptimizeInitializeTests(unittest.TestCase): @@ -26,6 +27,17 @@ def test_initializer_binary_has_no_unix_suffix(self) -> None: self.assertEqual(_initializer_binary(root, "darwin"), expected) self.assertEqual(_initializer_binary(root, "linux"), expected) + def test_initializer_seed_defaults_to_replay_value(self) -> None: + """The sweep is reproducible without additional seed configuration.""" + self.assertEqual(_parse_args([]).seed, 92) + + def test_initializer_seed_rejects_values_outside_uint64(self) -> None: + """Invalid seeds fail before any online experiment is created.""" + with self.assertRaises(argparse.ArgumentTypeError): + _parse_seed("-1") + with self.assertRaises(argparse.ArgumentTypeError): + _parse_seed("18446744073709551616") + def test_initializer_output_is_parsed(self) -> None: """The sweep extracts both the final size and volume profile.""" output = """Timeslice 1 has 12 spacelike faces. @@ -45,6 +57,24 @@ def test_initializer_output_requires_volume_profile(self) -> None: with self.assertRaisesRegex(RuntimeError, "did not contain a timeslice volume profile"): _parse_initializer_output(output) + def test_initializer_output_rejects_malformed_volume_entries(self) -> None: + """A partial profile is not accepted when one recognized row is malformed.""" + output = """Timeslice 1 has 12 spacelike faces. +Timeslice two has 24 spacelike faces. +Final number of simplices: 92""" + with self.assertRaisesRegex(RuntimeError, "malformed timeslice volume"): + _parse_initializer_output(output) + + def test_initializer_command_uses_the_recorded_seed(self) -> None: + """Every parameter pair forwards its replay seed to initialize.""" + command = _initializer_command( + Path("initialize"), + {"simplices": 12000, "foliations": 12, "seed": 92}, + initial_radius=1, + radial_factor=1.5, + ) + self.assertEqual(command[-2:], ["--seed", "92"]) + if __name__ == "__main__": unittest.main() From 31be85981d46e3812200beb1c433a4fde5c0455e Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Mon, 20 Jul 2026 16:36:44 -0700 Subject: [PATCH 04/13] fix: restore portable CI and named timeslice bounds - pass the Unix pkgx toolchain as one whitespace-separated package list - propagate verified Windows tools through PowerShell Core environment files - accept mutable lvalue bounds in generate_random_timeslice --- .github/workflows/ci.yml | 8 ++++---- include/Utilities.hpp | 6 +++--- tests/Utilities_test.cpp | 3 ++- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57ba506bdc..5acda0de99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,7 +89,7 @@ jobs: if: runner.os != 'Windows' uses: pkgxdev/setup@4d4ae97af87ccb39ab8be4e073dea697fef2c6f7 # v5.0.0 with: - +: | + +: >- doxygen.nl@${{ steps.tool-versions.outputs.doxygen }} graphviz.org@${{ steps.tool-versions.outputs.graphviz }} llvm.org@${{ steps.tool-versions.outputs.llvm }} @@ -141,8 +141,8 @@ jobs: throw 'The downloaded documentation archives did not contain doxygen.exe and dot.exe.' } - $doxygen.Directory.FullName | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - $dot.Directory.FullName | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + $doxygen.Directory.FullName >> $env:GITHUB_PATH + $dot.Directory.FullName >> $env:GITHUB_PATH - name: Set up Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 @@ -171,7 +171,7 @@ jobs: $env:GOBIN = $installDirectory $pinactModule = just --evaluate pinact_module go install $pinactModule - $installDirectory | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + $installDirectory >> $env:GITHUB_PATH - name: Select the Windows vcpkg triplet if: runner.os == 'Windows' diff --git a/include/Utilities.hpp b/include/Utilities.hpp index dfdc9745ad..c5b58e7018 100644 --- a/include/Utilities.hpp +++ b/include/Utilities.hpp @@ -415,12 +415,12 @@ namespace utilities /// @brief Generate a random timeslice template - [[nodiscard]] auto generate_random_timeslice(Generator& generator, - IntegerType&& t_max_timeslice) + [[nodiscard]] auto generate_random_timeslice(Generator& generator, + IntegerType t_max_timeslice) -> decltype(auto) { return generate_random_int(generator, static_cast(1), - std::forward(t_max_timeslice)); + t_max_timeslice); } // generate_random_timeslice() /// @brief Generate random real numbers by calling generate_random, preserves diff --git a/tests/Utilities_test.cpp b/tests/Utilities_test.cpp index d58c156454..05fe339a39 100644 --- a/tests/Utilities_test.cpp +++ b/tests/Utilities_test.cpp @@ -405,7 +405,8 @@ SCENARIO("Randomizing functions" * doctest::test_suite("utilities")) { cdt::Random generator{92}; CAPTURE(generator.seed()); - auto constexpr max = 256; + // Keep this mutable to cover ordinary named lvalue bounds. + auto max = 256; auto const value1 = generate_random_timeslice(generator, max); auto const value2 = generate_random_timeslice(generator, max); auto const value3 = generate_random_timeslice(generator, max); From f2ec25d89da7c9eda59a1d66c5308fef181f10b1 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Mon, 20 Jul 2026 16:46:49 -0700 Subject: [PATCH 05/13] fix(tooling): isolate Doxygen from cross-platform CI - Keep documentation validation separate from the portable just ci contract. - Launch pinned Doxygen and Graphviz ephemerally in the publishing workflow. - Document die_roll using its generic caller-supplied RNG contract. --- .github/workflows/ci.yml | 59 +---------------------------------- .github/workflows/doxygen.yml | 15 +-------- Justfile | 4 +-- README.md | 28 ++++++++--------- include/Utilities.hpp | 3 +- 5 files changed, 19 insertions(+), 90 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5acda0de99..b64fadf513 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,10 +76,6 @@ jobs: shell: bash run: | { - echo "doxygen=$(just --evaluate doxygen_version)" - echo "doxygen-windows-sha256=$(just --evaluate doxygen_windows_sha256)" - echo "graphviz=$(just --evaluate graphviz_version)" - echo "graphviz-windows-sha256=$(just --evaluate graphviz_windows_sha256)" echo "llvm=$(just --evaluate llvm_version)" echo "uv=$(just --evaluate uv_version)" echo "zizmor=$(just --evaluate zizmor_version)" @@ -89,60 +85,7 @@ jobs: if: runner.os != 'Windows' uses: pkgxdev/setup@4d4ae97af87ccb39ab8be4e073dea697fef2c6f7 # v5.0.0 with: - +: >- - doxygen.nl@${{ steps.tool-versions.outputs.doxygen }} - graphviz.org@${{ steps.tool-versions.outputs.graphviz }} - llvm.org@${{ steps.tool-versions.outputs.llvm }} - - - name: Install verified documentation tools on Windows - if: runner.os == 'Windows' - shell: pwsh - env: - DOXYGEN_SHA256: ${{ steps.tool-versions.outputs.doxygen-windows-sha256 }} - DOXYGEN_VERSION: ${{ steps.tool-versions.outputs.doxygen }} - GRAPHVIZ_SHA256: ${{ steps.tool-versions.outputs.graphviz-windows-sha256 }} - GRAPHVIZ_VERSION: ${{ steps.tool-versions.outputs.graphviz }} - run: | - $ErrorActionPreference = 'Stop' - - function Assert-ArchiveHash { - param( - [string] $Archive, - [string] $Expected - ) - - $actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $Archive).Hash.ToLowerInvariant() - if ($actual -ne $Expected.ToLowerInvariant()) { - throw "SHA-256 mismatch for $Archive`: expected $Expected, received $actual" - } - } - - $doxygenArchive = Join-Path $env:RUNNER_TEMP 'doxygen.zip' - $doxygenRoot = Join-Path $env:RUNNER_TEMP 'doxygen' - $doxygenTag = 'Release_' + ($env:DOXYGEN_VERSION -replace '\.', '_') - $doxygenBase = 'https://github.com/doxygen/doxygen/releases/download' - $doxygenUrl = "$doxygenBase/$doxygenTag/doxygen-$env:DOXYGEN_VERSION.windows.x64.bin.zip" - Invoke-WebRequest -Uri $doxygenUrl -OutFile $doxygenArchive - Assert-ArchiveHash -Archive $doxygenArchive -Expected $env:DOXYGEN_SHA256 - Expand-Archive -LiteralPath $doxygenArchive -DestinationPath $doxygenRoot - - $graphvizArchive = Join-Path $env:RUNNER_TEMP 'graphviz.zip' - $graphvizRoot = Join-Path $env:RUNNER_TEMP 'graphviz' - $graphvizPackage = "windows_10_cmake_Release_Graphviz-$env:GRAPHVIZ_VERSION-win64.zip" - $graphvizBase = 'https://gitlab.com/api/v4/projects/4207231/packages/generic/graphviz-releases' - $graphvizUrl = "$graphvizBase/$env:GRAPHVIZ_VERSION/$graphvizPackage" - Invoke-WebRequest -Uri $graphvizUrl -OutFile $graphvizArchive - Assert-ArchiveHash -Archive $graphvizArchive -Expected $env:GRAPHVIZ_SHA256 - Expand-Archive -LiteralPath $graphvizArchive -DestinationPath $graphvizRoot - - $doxygen = Get-ChildItem -Path $doxygenRoot -Filter doxygen.exe -Recurse | Select-Object -First 1 - $dot = Get-ChildItem -Path $graphvizRoot -Filter dot.exe -Recurse | Select-Object -First 1 - if ($null -eq $doxygen -or $null -eq $dot) { - throw 'The downloaded documentation archives did not contain doxygen.exe and dot.exe.' - } - - $doxygen.Directory.FullName >> $env:GITHUB_PATH - $dot.Directory.FullName >> $env:GITHUB_PATH + +: llvm.org@${{ steps.tool-versions.outputs.llvm }} - name: Set up Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 diff --git a/.github/workflows/doxygen.yml b/.github/workflows/doxygen.yml index d520aa970a..74d616b1fa 100644 --- a/.github/workflows/doxygen.yml +++ b/.github/workflows/doxygen.yml @@ -28,21 +28,8 @@ jobs: - name: Set up Just uses: ./.github/actions/setup-just - - name: Resolve documentation tool versions - id: tool-versions - shell: bash - run: | - { - echo "doxygen=$(just --evaluate doxygen_version)" - echo "graphviz=$(just --evaluate graphviz_version)" - } >> "$GITHUB_OUTPUT" - - - name: Set up documentation tools with pkgx + - name: Set up pkgx uses: pkgxdev/setup@4d4ae97af87ccb39ab8be4e073dea697fef2c6f7 # v5.0.0 - with: - +: | - doxygen.nl@${{ steps.tool-versions.outputs.doxygen }} - graphviz.org@${{ steps.tool-versions.outputs.graphviz }} - name: Build documentation run: just docs diff --git a/Justfile b/Justfile index 177de27ef8..7620a97db9 100644 --- a/Justfile +++ b/Justfile @@ -11,8 +11,6 @@ pinact_module := "github.com/suzuki-shunsuke/pinact/v4/cmd/pinact@v" + pinact_ve llvm_version := "22" doxygen_version := "1.17.0" graphviz_version := "15.1.0" -doxygen_windows_sha256 := "94594407c4cbca3049d76aacbb05d4a6f7d0f4e93c0de410b825d25ca5621c83" -graphviz_windows_sha256 := "c3ee71ff81ab97352082225574a140f20f5d6929d5f33d1097a1fe0e4161962a" zizmor_version := "1.26.1" primary_binary := if os_family() == "windows" { "out/build/reference/src/cdt.exe" } else { "out/build/reference/src/cdt" } rng_benchmark_binary := if os_family() == "windows" { "out/build/reference/tests/CDT_rng_benchmark.exe" } else { "out/build/reference/tests/CDT_rng_benchmark" } @@ -24,7 +22,7 @@ build: # Run fast, non-mutating local validation. [group('workflows')] -check: _justfile-check _format-check _yaml-check _action-lint _zizmor _whitespace-check _cmake-check docs-check release-check python-check semgrep semgrep-test +check: _justfile-check _format-check _yaml-check _action-lint _zizmor _whitespace-check _cmake-check release-check python-check semgrep semgrep-test @echo "Checks complete." # Run the comprehensive pre-commit/pre-push validation gate. diff --git a/README.md b/README.md index b0c46977ee..a97c095625 100644 --- a/README.md +++ b/README.md @@ -161,9 +161,9 @@ The smallest pkgx-assisted host setup is: - Python 3.12 and uv when checking or running the Python support scripts - Doxygen 1.17.0 and Graphviz 15.1.0 when checking or generating API documentation; pkgx can supply both -The pkgx launcher supplies Git, Bash, CMake, Ninja, Python, Doxygen, Graphviz, M4, Autoconf, Autoconf Archive, -Automake, GNU Libtool, Texinfo, and pkg-config. If pkgx is not installed, provide these tools conventionally through -a package manager such as [Homebrew] or apt: +The pkgx build and documentation launchers supply their required tools ephemerally, including Git, Bash, CMake, +Ninja, Python, Doxygen, Graphviz, M4, Autoconf, Autoconf Archive, Automake, GNU Libtool, Texinfo, and pkg-config. If +pkgx is not installed, provide these tools conventionally through a package manager such as [Homebrew] or apt: - Git - Bash @@ -203,18 +203,17 @@ just python-check # Check Python formatting, lint, and types just python-fix # Apply safe Ruff fixes and formatting ``` -`check` covers repository-wide C++ formatting, Python formatting/lint/type checks, strict Doxygen generation, -release metadata and citation fields, YAML, GitHub Actions syntax and security, whitespace, and CMake preset parsing. -`ci` adds the pinact policy check and the supported build/test contract. The GitHub Actions Ubuntu GCC, Ubuntu Clang, -macOS AppleClang, and Windows MSVC jobs all run the same `just ci` command. Windows continues to compile with native -MSVC; the locked Python environment supplies `clang-format` only as a source formatter. Install the developer tools -with Homebrew, use equivalent system packages, or let pkgx supply the Unix environment ephemerally; pkgx remains -optional. For example: +`check` covers repository-wide C++ formatting, Python formatting/lint/type checks, release metadata and citation +fields, YAML, GitHub Actions syntax and security, whitespace, and CMake preset parsing. `ci` adds the pinact policy +check and the supported build/test contract. Documentation validation remains available separately through +`just docs-check`. The GitHub Actions Ubuntu GCC, Ubuntu Clang, macOS AppleClang, and Windows MSVC jobs all run the +same `just ci` command. Windows continues to compile with native MSVC; the locked Python environment supplies +`clang-format` only as a source formatter. Install the developer tools with Homebrew, use equivalent system packages, +or let pkgx supply the Unix environment ephemerally; pkgx remains optional. For example: ```bash uv sync --locked --group dev -pkgx +just.systems@1.57.0 +git-scm.org +cmake.org +ninja-build.org +python.org \ - +doxygen.nl@1.17.0 +graphviz.org@15.1.0 +zizmor just check +pkgx +just.systems@1.57.0 +git-scm.org +cmake.org +ninja-build.org +python.org +zizmor just check ``` [pinact](https://github.com/suzuki-shunsuke/pinact) uses [`.pinact.yaml`](.pinact.yaml) to retain immutable action @@ -380,8 +379,9 @@ just docs-check To generate the same publishable output used by the documentation workflow, run `just docs`; it writes `docs/html/` only after strict generation succeeds. Both recipes require the pinned Doxygen and Graphviz versions and use pkgx ephemerally when matching local tools are unavailable. `USE_MATHJAX` allows [MathJax] to render LaTeX formulae, and -`HAVE_DOT` enables [GraphViz] diagrams. `just check` and therefore `just ci` include the non-mutating docs check on -every supported platform. The documentation workflow publishes the `just docs` output to the `gh-pages` branch. +`HAVE_DOT` enables [GraphViz] diagrams. Documentation validation is intentionally separate from the cross-platform +`just ci` contract. The documentation workflow runs `just docs` on Ubuntu and publishes its output to the `gh-pages` +branch. ## Citing CDT++ diff --git a/include/Utilities.hpp b/include/Utilities.hpp index c5b58e7018..6d5b647f7a 100644 --- a/include/Utilities.hpp +++ b/include/Utilities.hpp @@ -365,7 +365,8 @@ namespace utilities return triangulation; } // read_file - /// @brief Roll a die with PCG + /// @brief Roll a die using a caller-supplied + /// `std::uniform_random_bit_generator` template [[nodiscard]] inline auto die_roll(Generator& generator) { From 71cec75c1fb62ab93a09757a3bdc99395ac96a88 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Mon, 20 Jul 2026 17:01:59 -0700 Subject: [PATCH 06/13] fix(tooling): preserve Windows batch path through Bash --- Justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Justfile b/Justfile index 7620a97db9..a4217c25a6 100644 --- a/Justfile +++ b/Justfile @@ -18,7 +18,7 @@ rng_benchmark_binary := if os_family() == "windows" { "out/build/reference/tests # Build the supported configuration through the repository build script. [group('workflows')] build: - {{ if os_family() == "windows" { "cmd.exe //d //c scripts/build.bat" } else { "just _build-unix" } }} + {{ if os_family() == "windows" { "cmd.exe //d //c 'scripts\\build.bat'" } else { "just _build-unix" } }} # Run fast, non-mutating local validation. [group('workflows')] From 3b8effe53f2bd72643ae707a5e52b3759b31916c Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Mon, 20 Jul 2026 18:06:24 -0700 Subject: [PATCH 07/13] fix(tooling): stabilize Windows vcpkg validation - verify the pinned vcpkg executable with native certutil - align the action checkout with the repository vcpkg cache - limit MSVC build parallelism to the proven two-job configuration --- .github/workflows/ci.yml | 2 ++ scripts/bootstrap-vcpkg.bat | 7 ++++++- scripts/build.bat | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b64fadf513..1aa0b2b812 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,6 +123,8 @@ jobs: - name: Restore artifacts or set up vcpkg uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11.6 + with: + vcpkgDirectory: ${{ github.workspace }}/.cache/vcpkg - name: Run the canonical local CI contract env: diff --git a/scripts/bootstrap-vcpkg.bat b/scripts/bootstrap-vcpkg.bat index 0fe228d806..dad93a6596 100644 --- a/scripts/bootstrap-vcpkg.bat +++ b/scripts/bootstrap-vcpkg.bat @@ -28,6 +28,11 @@ IF ERRORLEVEL 1 ( EXIT /B 1 ) +IF NOT EXIST "%SystemRoot%\System32\certutil.exe" ( + echo certutil.exe is required to verify the vcpkg executable. 1>&2 + EXIT /B 1 +) + SET "BASELINE=" FOR /F "usebackq delims=" %%I IN (`powershell.exe -NoLogo -NoProfile -NonInteractive -Command "$json = Get-Content -Raw -LiteralPath $env:CDT_VCPKG_MANIFEST; $baseline = (ConvertFrom-Json -InputObject $json).'builtin-baseline'; if ($baseline -notmatch '^[0-9a-f]{40}$') { exit 1 }; $baseline"`) DO SET "BASELINE=%%I" IF NOT DEFINED BASELINE ( @@ -126,7 +131,7 @@ IF ERRORLEVEL 1 ( SET "EXPECTED_TOOL_SHA256=%TRUSTED_VCPKG_TOOL_SHA256_X64%" IF /I "%PROCESSOR_ARCHITECTURE%"=="ARM64" SET "EXPECTED_TOOL_SHA256=%TRUSTED_VCPKG_TOOL_SHA256_ARM64%" IF /I "%PROCESSOR_ARCHITEW6432%"=="ARM64" SET "EXPECTED_TOOL_SHA256=%TRUSTED_VCPKG_TOOL_SHA256_ARM64%" -powershell.exe -NoLogo -NoProfile -NonInteractive -Command "$actual = (Get-FileHash -Algorithm SHA256 -LiteralPath (Join-Path $env:VALIDATE_DIR 'vcpkg.exe')).Hash.ToLowerInvariant(); if ($actual -ne $env:EXPECTED_TOOL_SHA256) { exit 1 }" +"%SystemRoot%\System32\certutil.exe" -hashfile "%VALIDATE_DIR%\vcpkg.exe" SHA256 2>NUL | FINDSTR /I /X /C:"%EXPECTED_TOOL_SHA256%" >NUL IF ERRORLEVEL 1 ( ENDLOCAL EXIT /B 1 diff --git a/scripts/build.bat b/scripts/build.bat index c7731ee8d0..b4eb6aa4c6 100644 --- a/scripts/build.bat +++ b/scripts/build.bat @@ -26,7 +26,7 @@ SET "VCPKG_ROOT=%CDT_VCPKG_CACHE_DIR%" CD /D "%REPO_ROOT%" || EXIT /B 1 CALL :PREPARE_CMAKE_CACHE || EXIT /B 1 cmake --preset reference -S . || EXIT /B 1 -cmake --build --preset reference || EXIT /B 1 +cmake --build --preset reference --parallel 2 || EXIT /B 1 ctest --preset reference-smoke || EXIT /B 1 EXIT /B 0 From 29be4042eb15e3f4840acf9fdd33c8dd97cfb018 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Mon, 20 Jul 2026 18:51:33 -0700 Subject: [PATCH 08/13] fix(tooling): accept canonical vcpkg origins on Windows Capture the configured remote before comparing it so the Windows bootstrap does not reject the checkout prepared by run-vcpkg. Continue requiring the official microsoft/vcpkg repository while accepting canonical HTTPS URLs with or without the .git suffix. --- scripts/bootstrap-vcpkg.bat | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/scripts/bootstrap-vcpkg.bat b/scripts/bootstrap-vcpkg.bat index dad93a6596..ae5a2eb5fb 100644 --- a/scripts/bootstrap-vcpkg.bat +++ b/scripts/bootstrap-vcpkg.bat @@ -47,7 +47,7 @@ IF DEFINED CDT_VCPKG_CACHE_DIR ( ) IF EXIST "%VCPKG_DIR%\.git\." ( - git -C "%VCPKG_DIR%" remote get-url origin 2>NUL ^| FINDSTR /X /C:"https://github.com/microsoft/vcpkg.git" >NUL + CALL :VALIDATE_VCPKG_ORIGIN "%VCPKG_DIR%" IF ERRORLEVEL 1 ( echo Refusing to reuse %VCPKG_DIR% because its origin is not microsoft/vcpkg. 1>&2 EXIT /B 1 @@ -104,6 +104,21 @@ IF ERRORLEVEL 1 ( echo Bootstrapped vcpkg at %VCPKG_DIR% ^(%BASELINE%^) EXIT /B 0 +:VALIDATE_VCPKG_ORIGIN +SETLOCAL +SET "ORIGIN_URL=" +FOR /F "usebackq delims=" %%I IN (`git -C "%~1" remote get-url origin 2^>NUL`) DO SET "ORIGIN_URL=%%I" +IF /I "%ORIGIN_URL%"=="https://github.com/microsoft/vcpkg.git" ( + ENDLOCAL + EXIT /B 0 +) +IF /I "%ORIGIN_URL%"=="https://github.com/microsoft/vcpkg" ( + ENDLOCAL + EXIT /B 0 +) +ENDLOCAL +EXIT /B 1 + :VALIDATE_CACHED_VCPKG SETLOCAL SET "VALIDATE_DIR=%~1" From ca0ad40940f988848cf2852e747ef3bc7eccec5f Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Mon, 20 Jul 2026 19:14:36 -0700 Subject: [PATCH 09/13] fix(tooling): ignore untracked vcpkg metadata on Windows --- scripts/bootstrap-vcpkg.bat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bootstrap-vcpkg.bat b/scripts/bootstrap-vcpkg.bat index ae5a2eb5fb..a3dad8fb5c 100644 --- a/scripts/bootstrap-vcpkg.bat +++ b/scripts/bootstrap-vcpkg.bat @@ -53,7 +53,7 @@ IF EXIST "%VCPKG_DIR%\.git\." ( EXIT /B 1 ) - git -C "%VCPKG_DIR%" status --short 2>NUL ^| FINDSTR . >NUL + git -C "%VCPKG_DIR%" status --short --untracked-files=no 2>NUL ^| FINDSTR . >NUL IF NOT ERRORLEVEL 1 ( echo Refusing to reuse a modified vcpkg checkout at %VCPKG_DIR%. 1>&2 EXIT /B 1 From 049efab17599556e072b209516c76e519730ac2f Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Mon, 20 Jul 2026 20:28:19 -0700 Subject: [PATCH 10/13] fix(tooling): unify cross-platform vcpkg bootstrap Replace the platform-specific bootstrap validators with one Python implementation shared by Windows, macOS, and Linux. Preserve origin, baseline, executable-integrity, and version checks while allowing action-owned untracked vcpkg metadata. --- Justfile | 1 + README.md | 9 +- pyproject.toml | 1 + scripts/bootstrap-vcpkg.bat | 156 ----------- scripts/bootstrap-vcpkg.sh | 132 ---------- scripts/bootstrap_vcpkg.py | 365 ++++++++++++++++++++++++++ scripts/build.bat | 4 +- scripts/fast-build.bat | 4 +- scripts/prepare-vcpkg.sh | 16 +- scripts/tests/test_bootstrap_vcpkg.py | 187 +++++++++++++ 10 files changed, 578 insertions(+), 297 deletions(-) delete mode 100644 scripts/bootstrap-vcpkg.bat delete mode 100755 scripts/bootstrap-vcpkg.sh create mode 100644 scripts/bootstrap_vcpkg.py create mode 100644 scripts/tests/test_bootstrap_vcpkg.py diff --git a/Justfile b/Justfile index a4217c25a6..57c2499cfe 100644 --- a/Justfile +++ b/Justfile @@ -140,6 +140,7 @@ python-support-test: _ensure-uv # Smoke-test installed entry points without loading optional experiment dependencies. [group('workflows')] python-entrypoint-test: _ensure-uv + uv run --locked cdt-bootstrap-vcpkg --help >/dev/null uv run --locked cdt-optimize-initialize --help >/dev/null uv run --locked cdt-mnist-experiment --help >/dev/null diff --git a/README.md b/README.md index a97c095625..6e55efc67c 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ The smallest pkgx-assisted host setup is: - Xcode Command Line Tools on macOS, or a C++23 compiler and base build environment on Linux - pkgx - Just when invoking the recipes directly; `scripts/pkgx-build.sh` does not require it -- Python 3.12 and uv when checking or running the Python support scripts +- Python 3.12 for native dependency bootstrap, and uv when checking or running the Python support scripts - Doxygen 1.17.0 and Graphviz 15.1.0 when checking or generating API documentation; pkgx can supply both The pkgx build and documentation launchers supply their required tools ephemerally, including Git, Bash, CMake, @@ -226,17 +226,22 @@ development environment provides `clang-format`, `yamllint`, and `actionlint` co `vcpkg.json` is the dependency source of truth. Its `builtin-baseline` pins the official [`microsoft/vcpkg`](https://github.com/microsoft/vcpkg) registry commit used locally and in CI. The repository-local `.cache/vcpkg` checkout is disposable tool/cache infrastructure and must not be edited or committed. +The native build entry points delegate checkout provenance, baseline, and executable-integrity validation directly +to `scripts/bootstrap_vcpkg.py`, whose cross-platform fixtures run under `just check`. To update dependencies intentionally, bootstrap the current checkout, run the vcpkg baseline updater, review the manifest diff, and then rerun the complete build: ```bash -./scripts/bootstrap-vcpkg.sh +python3 scripts/bootstrap_vcpkg.py export VCPKG_ROOT="$PWD/.cache/vcpkg" "$VCPKG_ROOT/vcpkg" x-update-baseline ./scripts/build.sh ``` +On Windows, invoke the same implementation with `python.exe scripts\bootstrap_vcpkg.py`; `scripts\build.bat` and +`scripts\fast-build.bat` already do this directly. + CI uses `lukka/run-vcpkg`, which derives the vcpkg checkout commit from the same manifest baseline and supplies a binary cache. No separately maintained repository variable is required. diff --git a/pyproject.toml b/pyproject.toml index 2a8abc0f1d..3a202fde10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ requires-python = ">=3.12,<3.13" dependencies = [] [project.scripts] +cdt-bootstrap-vcpkg = "scripts.bootstrap_vcpkg:main" cdt-mnist-experiment = "scripts.mnist_experiment:main" cdt-optimize-initialize = "scripts.optimize_initialize:main" diff --git a/scripts/bootstrap-vcpkg.bat b/scripts/bootstrap-vcpkg.bat deleted file mode 100644 index a3dad8fb5c..0000000000 --- a/scripts/bootstrap-vcpkg.bat +++ /dev/null @@ -1,156 +0,0 @@ -@echo off - -SETLOCAL ENABLEEXTENSIONS DISABLEDELAYEDEXPANSION -SET "SCRIPT_DIR=%~dp0" -FOR %%I IN ("%SCRIPT_DIR%..") DO SET "REPO_ROOT=%%~fI" -SET "CDT_VCPKG_MANIFEST=%REPO_ROOT%\vcpkg.json" -SET "MODE=%~1" -REM GitHub release-asset SHA-256 digests for vcpkg-tool 2026-07-13. -SET "TRUSTED_VCPKG_TOOL_TAG=2026-07-13" -SET "TRUSTED_VCPKG_TOOL_SHA256_X64=67958c6a13a35130ff8035bef33097ffe3376a6708577a826cfa41fa592db611" -SET "TRUSTED_VCPKG_TOOL_SHA256_ARM64=8d87ed438db65b0015f624693612cb79d8c35908348c194984c23b63a2da0211" - -IF NOT DEFINED MODE SET "MODE=bootstrap" -IF /I NOT "%MODE%"=="bootstrap" IF /I NOT "%MODE%"=="--check" ( - echo Usage: %~nx0 [--check] 1>&2 - EXIT /B 2 -) - -WHERE git.exe >NUL 2>NUL -IF ERRORLEVEL 1 ( - echo git.exe is required to bootstrap vcpkg. 1>&2 - EXIT /B 1 -) - -WHERE powershell.exe >NUL 2>NUL -IF ERRORLEVEL 1 ( - echo powershell.exe is required to read the vcpkg manifest baseline. 1>&2 - EXIT /B 1 -) - -IF NOT EXIST "%SystemRoot%\System32\certutil.exe" ( - echo certutil.exe is required to verify the vcpkg executable. 1>&2 - EXIT /B 1 -) - -SET "BASELINE=" -FOR /F "usebackq delims=" %%I IN (`powershell.exe -NoLogo -NoProfile -NonInteractive -Command "$json = Get-Content -Raw -LiteralPath $env:CDT_VCPKG_MANIFEST; $baseline = (ConvertFrom-Json -InputObject $json).'builtin-baseline'; if ($baseline -notmatch '^[0-9a-f]{40}$') { exit 1 }; $baseline"`) DO SET "BASELINE=%%I" -IF NOT DEFINED BASELINE ( - echo Unable to read a 40-character builtin-baseline from %CDT_VCPKG_MANIFEST%. 1>&2 - EXIT /B 1 -) - -IF DEFINED CDT_VCPKG_CACHE_DIR ( - SET "VCPKG_DIR=%CDT_VCPKG_CACHE_DIR%" -) ELSE ( - SET "VCPKG_DIR=%REPO_ROOT%\.cache\vcpkg" -) - -IF EXIST "%VCPKG_DIR%\.git\." ( - CALL :VALIDATE_VCPKG_ORIGIN "%VCPKG_DIR%" - IF ERRORLEVEL 1 ( - echo Refusing to reuse %VCPKG_DIR% because its origin is not microsoft/vcpkg. 1>&2 - EXIT /B 1 - ) - - git -C "%VCPKG_DIR%" status --short --untracked-files=no 2>NUL ^| FINDSTR . >NUL - IF NOT ERRORLEVEL 1 ( - echo Refusing to reuse a modified vcpkg checkout at %VCPKG_DIR%. 1>&2 - EXIT /B 1 - ) - - git -C "%VCPKG_DIR%" rev-parse HEAD 2>NUL ^| FINDSTR /X /I /C:"%BASELINE%" >NUL - IF NOT ERRORLEVEL 1 IF EXIST "%VCPKG_DIR%\vcpkg.exe" ( - CALL :VALIDATE_CACHED_VCPKG "%VCPKG_DIR%" - IF NOT ERRORLEVEL 1 ( - echo Using vcpkg at %VCPKG_DIR% ^(%BASELINE%^) - EXIT /B 0 - ) - ) -) ELSE IF EXIST "%VCPKG_DIR%\." ( - DIR /B /A "%VCPKG_DIR%" 2>NUL ^| FINDSTR . >NUL - IF NOT ERRORLEVEL 1 ( - echo Refusing to initialize vcpkg in non-empty directory %VCPKG_DIR%. 1>&2 - EXIT /B 1 - ) -) - -IF /I "%MODE%"=="--check" ( - echo VCPKG_ROOT is not a clean official checkout at baseline %BASELINE%: %VCPKG_DIR% 1>&2 - EXIT /B 1 -) - -IF NOT EXIST "%VCPKG_DIR%\.git\." ( - IF NOT EXIST "%VCPKG_DIR%\." MD "%VCPKG_DIR%" - IF ERRORLEVEL 1 EXIT /B 1 - git -C "%VCPKG_DIR%" init - IF ERRORLEVEL 1 EXIT /B 1 - git -C "%VCPKG_DIR%" remote add origin https://github.com/microsoft/vcpkg.git - IF ERRORLEVEL 1 EXIT /B 1 -) - -git -C "%VCPKG_DIR%" fetch --depth 1 origin "%BASELINE%" -IF ERRORLEVEL 1 EXIT /B 1 -git -C "%VCPKG_DIR%" checkout --detach "%BASELINE%" -IF ERRORLEVEL 1 EXIT /B 1 -CALL "%VCPKG_DIR%\bootstrap-vcpkg.bat" -disableMetrics -IF ERRORLEVEL 1 EXIT /B 1 -CALL :VALIDATE_CACHED_VCPKG "%VCPKG_DIR%" -IF ERRORLEVEL 1 ( - echo Bootstrapped vcpkg does not match the tool release pinned by %BASELINE%. 1>&2 - EXIT /B 1 -) - -echo Bootstrapped vcpkg at %VCPKG_DIR% ^(%BASELINE%^) -EXIT /B 0 - -:VALIDATE_VCPKG_ORIGIN -SETLOCAL -SET "ORIGIN_URL=" -FOR /F "usebackq delims=" %%I IN (`git -C "%~1" remote get-url origin 2^>NUL`) DO SET "ORIGIN_URL=%%I" -IF /I "%ORIGIN_URL%"=="https://github.com/microsoft/vcpkg.git" ( - ENDLOCAL - EXIT /B 0 -) -IF /I "%ORIGIN_URL%"=="https://github.com/microsoft/vcpkg" ( - ENDLOCAL - EXIT /B 0 -) -ENDLOCAL -EXIT /B 1 - -:VALIDATE_CACHED_VCPKG -SETLOCAL -SET "VALIDATE_DIR=%~1" -SET "EXPECTED_TOOL_TAG=" -IF NOT EXIST "%VALIDATE_DIR%\scripts\vcpkg-tool-metadata.txt" ( - ENDLOCAL - EXIT /B 1 -) -FOR /F "usebackq tokens=1,* delims==" %%A IN ("%VALIDATE_DIR%\scripts\vcpkg-tool-metadata.txt") DO ( - IF /I "%%A"=="VCPKG_TOOL_RELEASE_TAG" SET "EXPECTED_TOOL_TAG=%%B" -) -IF NOT DEFINED EXPECTED_TOOL_TAG ( - ENDLOCAL - EXIT /B 1 -) -IF /I NOT "%EXPECTED_TOOL_TAG%"=="%TRUSTED_VCPKG_TOOL_TAG%" ( - ENDLOCAL - EXIT /B 1 -) -ECHO(%EXPECTED_TOOL_TAG%| FINDSTR /R /X "[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]" >NUL -IF ERRORLEVEL 1 ( - ENDLOCAL - EXIT /B 1 -) -SET "EXPECTED_TOOL_SHA256=%TRUSTED_VCPKG_TOOL_SHA256_X64%" -IF /I "%PROCESSOR_ARCHITECTURE%"=="ARM64" SET "EXPECTED_TOOL_SHA256=%TRUSTED_VCPKG_TOOL_SHA256_ARM64%" -IF /I "%PROCESSOR_ARCHITEW6432%"=="ARM64" SET "EXPECTED_TOOL_SHA256=%TRUSTED_VCPKG_TOOL_SHA256_ARM64%" -"%SystemRoot%\System32\certutil.exe" -hashfile "%VALIDATE_DIR%\vcpkg.exe" SHA256 2>NUL | FINDSTR /I /X /C:"%EXPECTED_TOOL_SHA256%" >NUL -IF ERRORLEVEL 1 ( - ENDLOCAL - EXIT /B 1 -) -"%VALIDATE_DIR%\vcpkg.exe" version 2>NUL | FINDSTR /B /I /L /C:"vcpkg package management program version %EXPECTED_TOOL_TAG%-" >NUL -SET "VALIDATION_RESULT=%ERRORLEVEL%" -ENDLOCAL & EXIT /B %VALIDATION_RESULT% diff --git a/scripts/bootstrap-vcpkg.sh b/scripts/bootstrap-vcpkg.sh deleted file mode 100755 index 50225734e4..0000000000 --- a/scripts/bootstrap-vcpkg.sh +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -repo_root="$(cd -- "${script_dir}/.." && pwd)" -manifest="${repo_root}/vcpkg.json" -vcpkg_dir="${CDT_VCPKG_CACHE_DIR:-${repo_root}/.cache/vcpkg}" -mode="${1:-bootstrap}" -trusted_vcpkg_tool_tag="2026-07-13" - -metadata_value() -{ - local key="${1}" - sed -nE "s/^${key}=(.*)$/\\1/p" "${vcpkg_dir}/scripts/vcpkg-tool-metadata.txt" -} - -compute_sha512() -{ - local file="${1}" - if command -v sha512sum >/dev/null; then - sha512sum "${file}" | awk '{print $1}' - elif command -v shasum >/dev/null; then - shasum -a 512 "${file}" | awk '{print $1}' - else - printf 'A SHA-512 utility is required to validate the cached vcpkg binary.\n' >&2 - return 1 - fi -} - -validate_cached_vcpkg() -{ - local metadata="${vcpkg_dir}/scripts/vcpkg-tool-metadata.txt" - local expected_sha_key="" - local expected_tag="" - local expected_sha="" - local actual_sha="" - - [[ -f "${metadata}" && -x "${vcpkg_dir}/vcpkg" ]] || return 1 - - expected_tag="$(metadata_value VCPKG_TOOL_RELEASE_TAG)" - [[ "${expected_tag}" == "${trusted_vcpkg_tool_tag}" ]] || return 1 - - case "$(uname -s):$(uname -m)" in - Darwin:*) - expected_sha_key="VCPKG_MACOS_SHA" - ;; - Linux:aarch64 | Linux:arm64) - expected_sha_key="VCPKG_GLIBC_ARM64_SHA" - ;; - Linux:x86_64 | Linux:amd64) - if ldd --version 2>&1 | grep -qi musl; then - expected_sha_key="VCPKG_MUSLC_SHA" - else - expected_sha_key="VCPKG_GLIBC_SHA" - fi - ;; - *) - return 1 - ;; - esac - - expected_sha="$(metadata_value "${expected_sha_key}")" - [[ "${expected_sha}" =~ ^[0-9a-f]{128}$ ]] || return 1 - actual_sha="$(compute_sha512 "${vcpkg_dir}/vcpkg")" || return 1 - [[ "${actual_sha}" == "${expected_sha}" ]] || return 1 - - "${vcpkg_dir}/vcpkg" version 2>/dev/null | - grep -q "^vcpkg package management program version ${trusted_vcpkg_tool_tag}-" -} - -if [[ "${mode}" != "bootstrap" && "${mode}" != "--check" ]]; then - printf 'Usage: %s [--check]\n' "$0" >&2 - exit 2 -fi - -baseline="$( - sed -nE 's/^[[:space:]]*"builtin-baseline":[[:space:]]*"([0-9a-f]{40})",?$/\1/p' \ - "${manifest}" -)" - -if [[ ! "${baseline}" =~ ^[0-9a-f]{40}$ ]]; then - printf 'Unable to read a 40-character builtin-baseline from %s\n' "${manifest}" >&2 - exit 1 -fi - -if [[ -d "${vcpkg_dir}/.git" ]]; then - origin="$(git -C "${vcpkg_dir}" remote get-url origin 2>/dev/null || true)" - if [[ "${origin}" != "https://github.com/microsoft/vcpkg.git" ]]; then - printf 'Refusing to reuse %s because its origin is not microsoft/vcpkg.\n' "${vcpkg_dir}" >&2 - exit 1 - fi - if [[ -n "$(git -C "${vcpkg_dir}" status --short --untracked-files=no)" ]]; then - printf 'Refusing to reuse a modified vcpkg checkout at %s.\n' "${vcpkg_dir}" >&2 - exit 1 - fi - if [[ -x "${vcpkg_dir}/vcpkg" ]] && - [[ "$(git -C "${vcpkg_dir}" rev-parse HEAD 2>/dev/null || true)" == "${baseline}" ]] && - validate_cached_vcpkg; then - printf 'Using vcpkg at %s (%s)\n' "${vcpkg_dir}" "${baseline}" - exit 0 - fi -elif [[ -d "${vcpkg_dir}" ]] && - [[ -n "$(find "${vcpkg_dir}" -mindepth 1 -maxdepth 1 -print -quit)" ]]; then - printf 'Refusing to initialize vcpkg in non-empty directory %s.\n' "${vcpkg_dir}" >&2 - exit 1 -fi - -if [[ "${mode}" == "--check" ]]; then - printf 'VCPKG_ROOT is not a clean official checkout at baseline %s: %s\n' \ - "${baseline}" "${vcpkg_dir}" >&2 - exit 1 -fi - -if [[ ! -d "${vcpkg_dir}/.git" ]]; then - mkdir -p "${vcpkg_dir}" - git -C "${vcpkg_dir}" init - git -C "${vcpkg_dir}" remote add origin https://github.com/microsoft/vcpkg.git -fi - -git -C "${vcpkg_dir}" fetch --depth 1 origin "${baseline}" -git -C "${vcpkg_dir}" checkout --detach "${baseline}" -rm -f -- "${vcpkg_dir}/vcpkg" -"${vcpkg_dir}/bootstrap-vcpkg.sh" -disableMetrics - -if ! validate_cached_vcpkg; then - printf 'Bootstrapped vcpkg does not match the tool release pinned by %s.\n' \ - "${baseline}" >&2 - exit 1 -fi - -printf 'Bootstrapped vcpkg at %s (%s)\n' "${vcpkg_dir}" "${baseline}" diff --git a/scripts/bootstrap_vcpkg.py b/scripts/bootstrap_vcpkg.py new file mode 100644 index 0000000000..307e4239d8 --- /dev/null +++ b/scripts/bootstrap_vcpkg.py @@ -0,0 +1,365 @@ +"""Bootstrap and validate the repository-pinned vcpkg checkout.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import platform +import re +import shutil +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Callable, Mapping, Sequence + +TRUSTED_TOOL_TAG = "2026-07-13" +OFFICIAL_ORIGINS = frozenset( + { + "https://github.com/microsoft/vcpkg", + "https://github.com/microsoft/vcpkg.git", + } +) +WINDOWS_TOOL_SHA256 = { + "amd64": "67958c6a13a35130ff8035bef33097ffe3376a6708577a826cfa41fa592db611", + "arm64": "8d87ed438db65b0015f624693612cb79d8c35908348c194984c23b63a2da0211", +} + + +class BootstrapError(RuntimeError): + """Report an invalid or unusable vcpkg checkout.""" + + +@dataclass(frozen=True) +class ToolSpec: + """Describe the trusted vcpkg executable for one host platform.""" + + executable_name: str + hash_algorithm: str + expected_digest: str + + +def _run( + command: Sequence[str | Path], + *, + cwd: Path | None = None, + capture_output: bool = False, +) -> subprocess.CompletedProcess[str]: + """Run a resolved repository-maintenance command without a shell.""" + resolved_command = [str(argument) for argument in command] + try: + return subprocess.run( # noqa: S603 - callers resolve executables or use validated checkout paths. + resolved_command, + cwd=cwd, + check=False, + capture_output=capture_output, + text=True, + ) + except OSError as error: + message = f"Unable to run {resolved_command[0]}: {error}" + raise BootstrapError(message) from error + + +def _require_executable(name: str) -> str: + """Resolve a required executable or fail with an actionable message.""" + executable = shutil.which(name) + if executable is None: + message = f"{name} is required to bootstrap vcpkg." + raise BootstrapError(message) + return executable + + +def _git(git: str, checkout: Path, *arguments: str, capture_output: bool = True) -> subprocess.CompletedProcess[str]: + """Run Git against a specific checkout.""" + return _run([git, "-C", checkout, *arguments], capture_output=capture_output) + + +def _git_output(git: str, checkout: Path, *arguments: str) -> str: + """Return trimmed Git output or raise a bootstrap error.""" + result = _git(git, checkout, *arguments) + if result.returncode != 0: + detail = result.stderr.strip() or result.stdout.strip() or "unknown Git error" + message = f"Git failed in {checkout}: {detail}" + raise BootstrapError(message) + return result.stdout.strip() + + +def _read_baseline(manifest: Path) -> str: + """Read and validate the pinned registry baseline.""" + try: + document = json.loads(manifest.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: + message = f"Unable to read {manifest}: {error}" + raise BootstrapError(message) from error + baseline = document.get("builtin-baseline") if isinstance(document, dict) else None + if not isinstance(baseline, str) or re.fullmatch(r"[0-9a-f]{40}", baseline) is None: + message = f"Unable to read a 40-character builtin-baseline from {manifest}." + raise BootstrapError(message) + return baseline + + +def _read_metadata(metadata_path: Path) -> dict[str, str]: + """Parse vcpkg tool metadata into key/value pairs.""" + try: + lines = metadata_path.read_text(encoding="utf-8").splitlines() + except OSError as error: + message = f"Unable to read vcpkg tool metadata at {metadata_path}." + raise BootstrapError(message) from error + metadata: dict[str, str] = {} + for line in lines: + key, separator, value = line.partition("=") + if separator: + metadata[key] = value + return metadata + + +def _host_uses_musl() -> bool: + """Return whether the Linux host reports musl as its C library.""" + ldd = shutil.which("ldd") + if ldd is None: + return False + result = _run([ldd, "--version"], capture_output=True) + return "musl" in f"{result.stdout}\n{result.stderr}".casefold() + + +def select_tool_spec( + metadata: Mapping[str, str], + *, + system_name: str | None = None, + machine_name: str | None = None, + uses_musl: bool | None = None, +) -> ToolSpec: + """Select the trusted executable and digest for a host platform.""" + system = (system_name or platform.system()).casefold() + machine = (machine_name or platform.machine()).casefold() + if system == "windows": + architecture = "arm64" if machine in {"aarch64", "arm64"} else "amd64" + if machine not in {"aarch64", "amd64", "arm64", "x86_64"}: + message = f"Unsupported Windows architecture for vcpkg: {machine}." + raise BootstrapError(message) + return ToolSpec("vcpkg.exe", "sha256", WINDOWS_TOOL_SHA256[architecture]) + + metadata_key: str + if system == "darwin": + metadata_key = "VCPKG_MACOS_SHA" + elif system == "linux" and machine in {"aarch64", "arm64"}: + metadata_key = "VCPKG_GLIBC_ARM64_SHA" + elif system == "linux" and machine in {"amd64", "x86_64"}: + metadata_key = "VCPKG_MUSLC_SHA" if (uses_musl if uses_musl is not None else _host_uses_musl()) else "VCPKG_GLIBC_SHA" + else: + message = f"Unsupported host for vcpkg: {system}/{machine}." + raise BootstrapError(message) + + expected_digest = metadata.get(metadata_key, "") + if re.fullmatch(r"[0-9a-f]{128}", expected_digest) is None: + message = f"Invalid {metadata_key} value in vcpkg tool metadata." + raise BootstrapError(message) + return ToolSpec("vcpkg", "sha512", expected_digest) + + +def _hash_file(path: Path, algorithm: str) -> str: + """Hash a file without loading the executable into memory.""" + digest = hashlib.new(algorithm) + try: + with path.open("rb") as handle: + for block in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(block) + except OSError as error: + message = f"Unable to hash trusted vcpkg executable {path}: {error}" + raise BootstrapError(message) from error + return digest.hexdigest() + + +def _read_tool_version(executable: Path) -> str: + """Run a hash-verified vcpkg executable and return its version output.""" + result = _run([executable, "version"], capture_output=True) + if result.returncode != 0: + message = f"Unable to read the vcpkg version from {executable}." + raise BootstrapError(message) + return result.stdout.strip() + + +def _validate_provenance(checkout: Path, git: str) -> None: + """Require an official origin and an unchanged tracked worktree.""" + origin = _git_output(git, checkout, "remote", "get-url", "origin") + if origin.casefold() not in OFFICIAL_ORIGINS: + message = f"Refusing to reuse {checkout} because its origin is not microsoft/vcpkg." + raise BootstrapError(message) + + diff = _git(git, checkout, "diff", "--quiet", "--no-ext-diff", "HEAD", "--") + if diff.returncode == 1: + status = _git_output(git, checkout, "status", "--short", "--untracked-files=no") + detail = f"\n{status}" if status else "" + message = f"Refusing to reuse a modified vcpkg checkout at {checkout}.{detail}" + raise BootstrapError(message) + if diff.returncode != 0: + detail = diff.stderr.strip() or diff.stdout.strip() or "unknown Git error" + message = f"Unable to verify tracked vcpkg files at {checkout}: {detail}" + raise BootstrapError(message) + + +def validate_checkout( + checkout: Path, + baseline: str, + *, + git: str | None = None, + tool_spec: ToolSpec | None = None, + version_reader: Callable[[Path], str] = _read_tool_version, +) -> None: + """Validate checkout provenance, revision, executable digest, and version.""" + git_executable = git or _require_executable("git") + if not (checkout / ".git").is_dir(): + message = f"No vcpkg Git checkout exists at {checkout}." + raise BootstrapError(message) + _validate_provenance(checkout, git_executable) + + actual_commit = _git_output(git_executable, checkout, "rev-parse", "HEAD") + if actual_commit.casefold() != baseline.casefold(): + message = f"vcpkg checkout {checkout} is at {actual_commit}, not pinned baseline {baseline}." + raise BootstrapError(message) + + metadata_path = checkout / "scripts" / "vcpkg-tool-metadata.txt" + metadata = _read_metadata(metadata_path) + if metadata.get("VCPKG_TOOL_RELEASE_TAG") != TRUSTED_TOOL_TAG: + message = f"vcpkg tool metadata at {metadata_path} is not release {TRUSTED_TOOL_TAG}." + raise BootstrapError(message) + selected_tool = tool_spec or select_tool_spec(metadata) + expected_length = hashlib.new(selected_tool.hash_algorithm).digest_size * 2 + if re.fullmatch(rf"[0-9a-f]{{{expected_length}}}", selected_tool.expected_digest) is None: + message = f"Invalid trusted {selected_tool.hash_algorithm} digest for {selected_tool.executable_name}." + raise BootstrapError(message) + + executable = checkout / selected_tool.executable_name + if not executable.is_file(): + message = f"Trusted vcpkg executable is missing: {executable}." + raise BootstrapError(message) + if platform.system() != "Windows" and not os.access(executable, os.X_OK): + message = f"Trusted vcpkg executable is not executable: {executable}." + raise BootstrapError(message) + actual_digest = _hash_file(executable, selected_tool.hash_algorithm) + if actual_digest != selected_tool.expected_digest: + message = f"vcpkg executable digest does not match the trusted {selected_tool.hash_algorithm} value: {executable}." + raise BootstrapError(message) + + expected_version_prefix = f"vcpkg package management program version {TRUSTED_TOOL_TAG}-" + version = version_reader(executable) + if not version.casefold().startswith(expected_version_prefix.casefold()): + message = f"Unexpected vcpkg tool version from {executable}: {version or ''}." + raise BootstrapError(message) + + +def _initialize_checkout(checkout: Path, git: str) -> None: + """Create an empty official vcpkg Git checkout.""" + checkout.mkdir(parents=True, exist_ok=True) + for arguments in (("init",), ("remote", "add", "origin", "https://github.com/microsoft/vcpkg.git")): + result = _git(git, checkout, *arguments, capture_output=False) + if result.returncode != 0: + message = f"Unable to initialize vcpkg at {checkout}." + raise BootstrapError(message) + + +def _update_checkout(checkout: Path, baseline: str, git: str) -> None: + """Fetch and check out exactly the manifest's pinned baseline.""" + commands = ( + ("fetch", "--depth", "1", "origin", baseline), + ("checkout", "--detach", baseline), + ) + for arguments in commands: + result = _git(git, checkout, *arguments, capture_output=False) + if result.returncode != 0: + message = f"Unable to update vcpkg at {checkout} to {baseline}." + raise BootstrapError(message) + + +def _bootstrap_tool(checkout: Path, tool_spec: ToolSpec) -> None: + """Build or download the vcpkg executable using the pinned source checkout.""" + executable = checkout / tool_spec.executable_name + executable.unlink(missing_ok=True) + if platform.system() == "Windows": + command = [os.environ.get("COMSPEC", "cmd.exe"), "/d", "/c", checkout / "bootstrap-vcpkg.bat", "-disableMetrics"] + else: + command = [checkout / "bootstrap-vcpkg.sh", "-disableMetrics"] + result = _run(command, cwd=checkout) + if result.returncode != 0: + message = f"Unable to bootstrap vcpkg at {checkout}." + raise BootstrapError(message) + + +def bootstrap_vcpkg(repository_root: Path, *, check_only: bool = False) -> Path: + """Validate or provision the repository-pinned vcpkg checkout.""" + git = _require_executable("git") + manifest = repository_root / "vcpkg.json" + baseline = _read_baseline(manifest) + configured_checkout = os.environ.get("CDT_VCPKG_CACHE_DIR") + checkout = Path(configured_checkout).resolve() if configured_checkout else (repository_root / ".cache" / "vcpkg").resolve() + + if (checkout / ".git").is_dir(): + _validate_provenance(checkout, git) + try: + validate_checkout(checkout, baseline, git=git) + except BootstrapError: + if check_only: + raise + else: + print(f"Using vcpkg at {checkout} ({baseline})") + return checkout + elif checkout.is_dir() and any(checkout.iterdir()): + message = f"Refusing to initialize vcpkg in non-empty directory {checkout}." + raise BootstrapError(message) + + if check_only: + message = f"VCPKG_ROOT is not a validated official checkout at baseline {baseline}: {checkout}" + raise BootstrapError(message) + + if not (checkout / ".git").is_dir(): + _initialize_checkout(checkout, git) + _update_checkout(checkout, baseline, git) + metadata = _read_metadata(checkout / "scripts" / "vcpkg-tool-metadata.txt") + tool_spec = select_tool_spec(metadata) + _bootstrap_tool(checkout, tool_spec) + validate_checkout(checkout, baseline, git=git, tool_spec=tool_spec) + print(f"Bootstrapped vcpkg at {checkout} ({baseline})") + return checkout + + +def _parse_args(argv: Sequence[str] | None) -> argparse.Namespace: + """Parse the supported bootstrap mode.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true", help="validate the existing checkout without changing it") + return parser.parse_args(argv) + + +def _find_repository_root(start: Path | None = None) -> Path: + """Find the nearest vcpkg manifest for direct and installed invocations.""" + search_roots = [Path(__file__).resolve().parent.parent, Path.cwd().resolve()] if start is None else [start.resolve()] + visited: set[Path] = set() + for search_root in search_roots: + for candidate in (search_root, *search_root.parents): + if candidate in visited: + continue + visited.add(candidate) + if (candidate / "vcpkg.json").is_file(): + return candidate + message = "Unable to find vcpkg.json from the working directory or bootstrap script location." + raise BootstrapError(message) + + +def main(argv: Sequence[str] | None = None) -> int: + """Run the vcpkg bootstrap or validation command.""" + args = _parse_args(argv) + try: + repository_root = _find_repository_root() + bootstrap_vcpkg(repository_root, check_only=args.check) + except (BootstrapError, OSError) as error: + print(error, file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/build.bat b/scripts/build.bat index b4eb6aa4c6..7a80c4b87b 100644 --- a/scripts/build.bat +++ b/scripts/build.bat @@ -12,14 +12,14 @@ IF DEFINED CDT_VCPKG_CACHE_DIR ( IF DEFINED VCPKG_ROOT ( SET "CDT_VCPKG_CACHE_DIR=%VCPKG_ROOT%" - CALL "%SCRIPT_DIR%bootstrap-vcpkg.bat" --check >NUL 2>NUL + python.exe "%SCRIPT_DIR%bootstrap_vcpkg.py" --check >NUL 2>NUL IF NOT ERRORLEVEL 1 GOTO VCPKG_READY echo Ignoring VCPKG_ROOT=%VCPKG_ROOT%; using the repository-pinned checkout instead. 1>&2 ) SET "CDT_VCPKG_CACHE_DIR=%PINNED_VCPKG_ROOT%" SET "VCPKG_ROOT=%PINNED_VCPKG_ROOT%" -CALL "%SCRIPT_DIR%bootstrap-vcpkg.bat" || EXIT /B 1 +python.exe "%SCRIPT_DIR%bootstrap_vcpkg.py" || EXIT /B 1 :VCPKG_READY SET "VCPKG_ROOT=%CDT_VCPKG_CACHE_DIR%" diff --git a/scripts/fast-build.bat b/scripts/fast-build.bat index e9c8390312..a38788f0d6 100644 --- a/scripts/fast-build.bat +++ b/scripts/fast-build.bat @@ -12,14 +12,14 @@ IF DEFINED CDT_VCPKG_CACHE_DIR ( IF DEFINED VCPKG_ROOT ( SET "CDT_VCPKG_CACHE_DIR=%VCPKG_ROOT%" - CALL "%SCRIPT_DIR%bootstrap-vcpkg.bat" --check >NUL 2>NUL + python.exe "%SCRIPT_DIR%bootstrap_vcpkg.py" --check >NUL 2>NUL IF NOT ERRORLEVEL 1 GOTO VCPKG_READY echo Ignoring VCPKG_ROOT=%VCPKG_ROOT%; using the repository-pinned checkout instead. 1>&2 ) SET "CDT_VCPKG_CACHE_DIR=%PINNED_VCPKG_ROOT%" SET "VCPKG_ROOT=%PINNED_VCPKG_ROOT%" -CALL "%SCRIPT_DIR%bootstrap-vcpkg.bat" || EXIT /B 1 +python.exe "%SCRIPT_DIR%bootstrap_vcpkg.py" || EXIT /B 1 :VCPKG_READY SET "VCPKG_ROOT=%CDT_VCPKG_CACHE_DIR%" diff --git a/scripts/prepare-vcpkg.sh b/scripts/prepare-vcpkg.sh index 9b60332db7..263e67af38 100644 --- a/scripts/prepare-vcpkg.sh +++ b/scripts/prepare-vcpkg.sh @@ -35,10 +35,20 @@ prepare_vcpkg() { local repository_root="${1:?repository root is required}" local pinned_vcpkg_root="${CDT_VCPKG_CACHE_DIR:-${repository_root}/.cache/vcpkg}" - local bootstrap_script="${repository_root}/scripts/bootstrap-vcpkg.sh" + local bootstrap_script="${repository_root}/scripts/bootstrap_vcpkg.py" + local python_executable + + if command -v python3 >/dev/null; then + python_executable="$(command -v python3)" + elif command -v python >/dev/null; then + python_executable="$(command -v python)" + else + printf 'Python is required to bootstrap vcpkg.\n' >&2 + return 1 + fi if [[ -n "${VCPKG_ROOT:-}" ]] && - CDT_VCPKG_CACHE_DIR="${VCPKG_ROOT}" "${bootstrap_script}" --check 2>/dev/null; then + CDT_VCPKG_CACHE_DIR="${VCPKG_ROOT}" "${python_executable}" "${bootstrap_script}" --check 2>/dev/null; then VCPKG_ROOT="$(cd -- "${VCPKG_ROOT}" && pwd -P)" export VCPKG_ROOT return @@ -50,7 +60,7 @@ prepare_vcpkg() fi export VCPKG_ROOT="${pinned_vcpkg_root}" - "${bootstrap_script}" + "${python_executable}" "${bootstrap_script}" VCPKG_ROOT="$(cd -- "${VCPKG_ROOT}" && pwd -P)" export VCPKG_ROOT } diff --git a/scripts/tests/test_bootstrap_vcpkg.py b/scripts/tests/test_bootstrap_vcpkg.py new file mode 100644 index 0000000000..d24d6ac850 --- /dev/null +++ b/scripts/tests/test_bootstrap_vcpkg.py @@ -0,0 +1,187 @@ +"""Tests for cross-platform vcpkg checkout validation.""" + +from __future__ import annotations + +import hashlib +import shutil +import subprocess +import tempfile +import unittest +from pathlib import Path +from typing import override +from unittest import mock + +from scripts import bootstrap_vcpkg + + +class BootstrapVcpkgTests(unittest.TestCase): + """Exercise repository provenance and tool-integrity validation.""" + + git: str + + @override + def setUp(self) -> None: + """Require Git for repository-backed fixtures.""" + git = shutil.which("git") + if git is None: + self.skipTest("git is required") + self.git = git + + def _git(self, checkout: Path, *arguments: str) -> str: + """Run Git in a test checkout and return stdout.""" + result = subprocess.run( # noqa: S603 - self.git is resolved with shutil.which. + [self.git, "-C", str(checkout), *arguments], + check=True, + capture_output=True, + text=True, + ) + return result.stdout.strip() + + def _make_checkout(self, root: Path) -> tuple[str, bootstrap_vcpkg.ToolSpec]: + """Create a minimal official checkout with a hash-verifiable tool.""" + self._git(root, "init") + self._git(root, "config", "user.email", "test@example.com") + self._git(root, "config", "user.name", "CDT++ tests") + metadata = root / "scripts" / "vcpkg-tool-metadata.txt" + metadata.parent.mkdir(parents=True) + metadata.write_text(f"VCPKG_TOOL_RELEASE_TAG={bootstrap_vcpkg.TRUSTED_TOOL_TAG}\n", encoding="utf-8") + tracked = root / "tracked.txt" + tracked.write_text("tracked\n", encoding="utf-8") + self._git(root, "add", "scripts/vcpkg-tool-metadata.txt", "tracked.txt") + self._git(root, "commit", "-m", "fixture") + self._git(root, "remote", "add", "origin", "https://github.com/microsoft/vcpkg.git") + + tool_content = b"fixture vcpkg tool" + tool = root / "vcpkg-test" + tool.write_bytes(tool_content) + tool.chmod(0o755) + tool_spec = bootstrap_vcpkg.ToolSpec("vcpkg-test", "sha256", hashlib.sha256(tool_content).hexdigest()) + return self._git(root, "rev-parse", "HEAD"), tool_spec + + @staticmethod + def _valid_version(_executable: Path) -> str: + """Return version output accepted by the validator.""" + return f"vcpkg package management program version {bootstrap_vcpkg.TRUSTED_TOOL_TAG}-fixture" + + def test_accepts_official_clean_checkout_with_untracked_metadata(self) -> None: + """Action-owned untracked metadata does not invalidate tracked sources.""" + with tempfile.TemporaryDirectory() as temp_dir: + checkout = Path(temp_dir) + baseline, tool_spec = self._make_checkout(checkout) + (checkout / "vcpkgLastBuiltCommitId").write_text(baseline, encoding="utf-8") + + bootstrap_vcpkg.validate_checkout( + checkout, + baseline, + git=self.git, + tool_spec=tool_spec, + version_reader=self._valid_version, + ) + + def test_rejects_tracked_modification_with_path(self) -> None: + """Tracked changes fail and identify the modified file.""" + with tempfile.TemporaryDirectory() as temp_dir: + checkout = Path(temp_dir) + baseline, tool_spec = self._make_checkout(checkout) + (checkout / "tracked.txt").write_text("modified\n", encoding="utf-8") + + with self.assertRaisesRegex(bootstrap_vcpkg.BootstrapError, r"modified[\s\S]*tracked\.txt"): + bootstrap_vcpkg.validate_checkout( + checkout, + baseline, + git=self.git, + tool_spec=tool_spec, + version_reader=self._valid_version, + ) + + def test_rejects_nonofficial_origin(self) -> None: + """A checkout from another repository is rejected.""" + with tempfile.TemporaryDirectory() as temp_dir: + checkout = Path(temp_dir) + baseline, tool_spec = self._make_checkout(checkout) + self._git(checkout, "remote", "set-url", "origin", "https://example.com/vcpkg.git") + + with self.assertRaisesRegex(bootstrap_vcpkg.BootstrapError, "origin is not microsoft/vcpkg"): + bootstrap_vcpkg.validate_checkout( + checkout, + baseline, + git=self.git, + tool_spec=tool_spec, + version_reader=self._valid_version, + ) + + def test_rejects_wrong_baseline(self) -> None: + """A clean checkout at another commit is rejected.""" + with tempfile.TemporaryDirectory() as temp_dir: + checkout = Path(temp_dir) + _baseline, tool_spec = self._make_checkout(checkout) + + with self.assertRaisesRegex(bootstrap_vcpkg.BootstrapError, "not pinned baseline"): + bootstrap_vcpkg.validate_checkout( + checkout, + "0" * 40, + git=self.git, + tool_spec=tool_spec, + version_reader=self._valid_version, + ) + + def test_rejects_tool_digest_mismatch(self) -> None: + """An executable that differs from its trusted digest is rejected.""" + with tempfile.TemporaryDirectory() as temp_dir: + checkout = Path(temp_dir) + baseline, tool_spec = self._make_checkout(checkout) + invalid_spec = bootstrap_vcpkg.ToolSpec(tool_spec.executable_name, tool_spec.hash_algorithm, "0" * 64) + + with self.assertRaisesRegex(bootstrap_vcpkg.BootstrapError, "digest does not match"): + bootstrap_vcpkg.validate_checkout( + checkout, + baseline, + git=self.git, + tool_spec=invalid_spec, + version_reader=self._valid_version, + ) + + def test_selects_windows_tool_hashes_by_architecture(self) -> None: + """Windows x64 and ARM64 select their pinned release assets.""" + x64 = bootstrap_vcpkg.select_tool_spec({}, system_name="Windows", machine_name="AMD64") + arm64 = bootstrap_vcpkg.select_tool_spec({}, system_name="Windows", machine_name="ARM64") + + self.assertEqual(x64.executable_name, "vcpkg.exe") + self.assertEqual(x64.expected_digest, bootstrap_vcpkg.WINDOWS_TOOL_SHA256["amd64"]) + self.assertEqual(arm64.expected_digest, bootstrap_vcpkg.WINDOWS_TOOL_SHA256["arm64"]) + + def test_finds_repository_root_from_nested_working_directory(self) -> None: + """An installed entry point locates the checkout independently of its module path.""" + with tempfile.TemporaryDirectory() as temp_dir: + repository_root = Path(temp_dir) + nested_directory = repository_root / "nested" / "directory" + nested_directory.mkdir(parents=True) + (repository_root / "vcpkg.json").write_text("{}\n", encoding="utf-8") + + self.assertEqual( + bootstrap_vcpkg._find_repository_root(nested_directory), # noqa: SLF001 - focused repository-discovery contract. + repository_root.resolve(), + ) + + def test_empty_cache_override_uses_repository_cache(self) -> None: + """An empty cache environment value behaves like an unset override.""" + with ( + tempfile.TemporaryDirectory() as temp_dir, + mock.patch.dict( + bootstrap_vcpkg.os.environ, + {"CDT_VCPKG_CACHE_DIR": ""}, + ), + ): + repository_root = Path(temp_dir) + (repository_root / "vcpkg.json").write_text( + '{"builtin-baseline": "0000000000000000000000000000000000000000"}\n', + encoding="utf-8", + ) + + with self.assertRaises(bootstrap_vcpkg.BootstrapError) as raised: + bootstrap_vcpkg.bootstrap_vcpkg(repository_root, check_only=True) + self.assertIn(str(repository_root / ".cache" / "vcpkg"), str(raised.exception)) + + +if __name__ == "__main__": + unittest.main() From 1eb322290d1036a67f17bccc9b861a1ec57da0d7 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Mon, 20 Jul 2026 20:51:48 -0700 Subject: [PATCH 11/13] fix(tooling): stabilize sanitizer and Windows validation - provision sanitizer CMake and Ninja from shared Justfile pins - isolate sanitizer builds from pkgx transitive build variables - canonicalize Windows cache paths and avoid false secret classification --- .github/workflows/_sanitizer.yml | 25 ++++++++++++++++++------- Justfile | 2 ++ scripts/bootstrap_vcpkg.py | 8 ++++---- scripts/pkgx-build.sh | 6 ++++-- scripts/sanitizer.sh | 1 + scripts/tests/test_bootstrap_vcpkg.py | 7 ++++--- 6 files changed, 33 insertions(+), 16 deletions(-) diff --git a/.github/workflows/_sanitizer.yml b/.github/workflows/_sanitizer.yml index 352b329932..a1d3062a86 100644 --- a/.github/workflows/_sanitizer.yml +++ b/.github/workflows/_sanitizer.yml @@ -71,18 +71,29 @@ jobs: - name: Set up Just uses: ./.github/actions/setup-just - - name: Resolve LLVM version - id: llvm-version + - name: Resolve sanitizer tool versions + id: tool-versions shell: bash - run: echo "version=$(just --evaluate llvm_version)" >> "$GITHUB_OUTPUT" + run: | + { + echo "llvm=$(just --evaluate llvm_version)" + echo "cmake=$(just --evaluate cmake_version)" + echo "ninja=$(just --evaluate ninja_version)" + } >> "$GITHUB_OUTPUT" - - name: Set up Clang + - name: Set up sanitizer toolchain with pkgx uses: pkgxdev/setup@4d4ae97af87ccb39ab8be4e073dea697fef2c6f7 # v5.0.0 with: - +: llvm.org@${{ steps.llvm-version.outputs.version }} + +: | + llvm.org@${{ steps.tool-versions.outputs.llvm }} + cmake.org@${{ steps.tool-versions.outputs.cmake }} + ninja-build.org@${{ steps.tool-versions.outputs.ninja }} - - name: Report compiler version - run: clang --version + - name: Report sanitizer toolchain versions + run: | + clang --version + cmake --version + ninja --version - name: Restore artifacts or set up vcpkg uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11.6 diff --git a/Justfile b/Justfile index 57c2499cfe..a6d79a7822 100644 --- a/Justfile +++ b/Justfile @@ -9,6 +9,8 @@ uv_version := "0.11.29" pinact_version := "4.1.0" pinact_module := "github.com/suzuki-shunsuke/pinact/v4/cmd/pinact@v" + pinact_version llvm_version := "22" +cmake_version := "4.4.0" +ninja_version := "1.13.2" doxygen_version := "1.17.0" graphviz_version := "15.1.0" zizmor_version := "1.26.1" diff --git a/scripts/bootstrap_vcpkg.py b/scripts/bootstrap_vcpkg.py index 307e4239d8..bb33281d28 100644 --- a/scripts/bootstrap_vcpkg.py +++ b/scripts/bootstrap_vcpkg.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: from collections.abc import Callable, Mapping, Sequence -TRUSTED_TOOL_TAG = "2026-07-13" +VCPKG_TOOL_RELEASE = "2026-07-13" OFFICIAL_ORIGINS = frozenset( { "https://github.com/microsoft/vcpkg", @@ -225,8 +225,8 @@ def validate_checkout( metadata_path = checkout / "scripts" / "vcpkg-tool-metadata.txt" metadata = _read_metadata(metadata_path) - if metadata.get("VCPKG_TOOL_RELEASE_TAG") != TRUSTED_TOOL_TAG: - message = f"vcpkg tool metadata at {metadata_path} is not release {TRUSTED_TOOL_TAG}." + if metadata.get("VCPKG_TOOL_RELEASE_TAG") != VCPKG_TOOL_RELEASE: + message = f"vcpkg tool metadata at {metadata_path} is not release {VCPKG_TOOL_RELEASE}." raise BootstrapError(message) selected_tool = tool_spec or select_tool_spec(metadata) expected_length = hashlib.new(selected_tool.hash_algorithm).digest_size * 2 @@ -246,7 +246,7 @@ def validate_checkout( message = f"vcpkg executable digest does not match the trusted {selected_tool.hash_algorithm} value: {executable}." raise BootstrapError(message) - expected_version_prefix = f"vcpkg package management program version {TRUSTED_TOOL_TAG}-" + expected_version_prefix = f"vcpkg package management program version {VCPKG_TOOL_RELEASE}-" version = version_reader(executable) if not version.casefold().startswith(expected_version_prefix.casefold()): message = f"Unexpected vcpkg tool version from {executable}: {version or ''}." diff --git a/scripts/pkgx-build.sh b/scripts/pkgx-build.sh index fab4ecf696..319b7ecc63 100755 --- a/scripts/pkgx-build.sh +++ b/scripts/pkgx-build.sh @@ -5,11 +5,13 @@ set -euo pipefail script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd -- "${script_dir}/.." && pwd)" +cmake_version="$(just --justfile "${repo_root}/Justfile" --evaluate cmake_version)" +ninja_version="$(just --justfile "${repo_root}/Justfile" --evaluate ninja_version)" pkgx_tools=( +git-scm.org@2.55.0 - +cmake.org@4.4.0 - +ninja-build.org@1.13.2 + "+cmake.org@${cmake_version}" + "+ninja-build.org@${ninja_version}" +python.org@3.11.15 +gnu.org/m4@1.4.21 +gnu.org/autoconf@2.73.0 diff --git a/scripts/sanitizer.sh b/scripts/sanitizer.sh index 951f83931c..504c94776f 100755 --- a/scripts/sanitizer.sh +++ b/scripts/sanitizer.sh @@ -27,6 +27,7 @@ if [[ "$(uname -s)" != "Linux" ]]; then fi source "${script_dir}/prepare-vcpkg.sh" +prepare_reference_environment prepare_vcpkg "${repo_root}" rm -rf -- "${build_dir}" diff --git a/scripts/tests/test_bootstrap_vcpkg.py b/scripts/tests/test_bootstrap_vcpkg.py index d24d6ac850..daf35591fa 100644 --- a/scripts/tests/test_bootstrap_vcpkg.py +++ b/scripts/tests/test_bootstrap_vcpkg.py @@ -44,7 +44,7 @@ def _make_checkout(self, root: Path) -> tuple[str, bootstrap_vcpkg.ToolSpec]: self._git(root, "config", "user.name", "CDT++ tests") metadata = root / "scripts" / "vcpkg-tool-metadata.txt" metadata.parent.mkdir(parents=True) - metadata.write_text(f"VCPKG_TOOL_RELEASE_TAG={bootstrap_vcpkg.TRUSTED_TOOL_TAG}\n", encoding="utf-8") + metadata.write_text(f"VCPKG_TOOL_RELEASE_TAG={bootstrap_vcpkg.VCPKG_TOOL_RELEASE}\n", encoding="utf-8") tracked = root / "tracked.txt" tracked.write_text("tracked\n", encoding="utf-8") self._git(root, "add", "scripts/vcpkg-tool-metadata.txt", "tracked.txt") @@ -61,7 +61,7 @@ def _make_checkout(self, root: Path) -> tuple[str, bootstrap_vcpkg.ToolSpec]: @staticmethod def _valid_version(_executable: Path) -> str: """Return version output accepted by the validator.""" - return f"vcpkg package management program version {bootstrap_vcpkg.TRUSTED_TOOL_TAG}-fixture" + return f"vcpkg package management program version {bootstrap_vcpkg.VCPKG_TOOL_RELEASE}-fixture" def test_accepts_official_clean_checkout_with_untracked_metadata(self) -> None: """Action-owned untracked metadata does not invalidate tracked sources.""" @@ -180,7 +180,8 @@ def test_empty_cache_override_uses_repository_cache(self) -> None: with self.assertRaises(bootstrap_vcpkg.BootstrapError) as raised: bootstrap_vcpkg.bootstrap_vcpkg(repository_root, check_only=True) - self.assertIn(str(repository_root / ".cache" / "vcpkg"), str(raised.exception)) + expected_cache = (repository_root / ".cache" / "vcpkg").resolve() + self.assertIn(str(expected_cache), str(raised.exception)) if __name__ == "__main__": From 6ac07d04400738c44989364776517eca3146707a Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Mon, 20 Jul 2026 21:49:43 -0700 Subject: [PATCH 12/13] fix(tooling): fold sanitizer pkgx package input --- .github/workflows/_sanitizer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_sanitizer.yml b/.github/workflows/_sanitizer.yml index a1d3062a86..74bc5efb91 100644 --- a/.github/workflows/_sanitizer.yml +++ b/.github/workflows/_sanitizer.yml @@ -84,7 +84,7 @@ jobs: - name: Set up sanitizer toolchain with pkgx uses: pkgxdev/setup@4d4ae97af87ccb39ab8be4e073dea697fef2c6f7 # v5.0.0 with: - +: | + +: >- llvm.org@${{ steps.tool-versions.outputs.llvm }} cmake.org@${{ steps.tool-versions.outputs.cmake }} ninja-build.org@${{ steps.tool-versions.outputs.ninja }} From a578231204000f58f689b20cd11d89e8c254e6e6 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Mon, 20 Jul 2026 21:58:51 -0700 Subject: [PATCH 13/13] fix(tooling): isolate sanitizer pkgx environment --- .github/workflows/_sanitizer.yml | 23 +---------------------- Justfile | 11 ++++++++++- scripts/sanitizer.sh | 3 +++ 3 files changed, 14 insertions(+), 23 deletions(-) diff --git a/.github/workflows/_sanitizer.yml b/.github/workflows/_sanitizer.yml index 74bc5efb91..2a3f84c812 100644 --- a/.github/workflows/_sanitizer.yml +++ b/.github/workflows/_sanitizer.yml @@ -71,29 +71,8 @@ jobs: - name: Set up Just uses: ./.github/actions/setup-just - - name: Resolve sanitizer tool versions - id: tool-versions - shell: bash - run: | - { - echo "llvm=$(just --evaluate llvm_version)" - echo "cmake=$(just --evaluate cmake_version)" - echo "ninja=$(just --evaluate ninja_version)" - } >> "$GITHUB_OUTPUT" - - - name: Set up sanitizer toolchain with pkgx + - name: Set up pkgx uses: pkgxdev/setup@4d4ae97af87ccb39ab8be4e073dea697fef2c6f7 # v5.0.0 - with: - +: >- - llvm.org@${{ steps.tool-versions.outputs.llvm }} - cmake.org@${{ steps.tool-versions.outputs.cmake }} - ninja-build.org@${{ steps.tool-versions.outputs.ninja }} - - - name: Report sanitizer toolchain versions - run: | - clang --version - cmake --version - ninja --version - name: Restore artifacts or set up vcpkg uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11.6 diff --git a/Justfile b/Justfile index a6d79a7822..aefe49247c 100644 --- a/Justfile +++ b/Justfile @@ -111,7 +111,16 @@ semgrep-test: _ensure-uv # Build and exercise one supported Linux sanitizer configuration. [group('workflows')] sanitize kind: - ./scripts/sanitizer.sh {{ kind }} + #!/usr/bin/env bash + set -euo pipefail + if command -v pkgx >/dev/null; then + exec pkgx \ + "+llvm.org@{{ llvm_version }}" \ + "+cmake.org@{{ cmake_version }}" \ + "+ninja-build.org@{{ ninja_version }}" \ + -- ./scripts/sanitizer.sh "{{ kind }}" + fi + exec ./scripts/sanitizer.sh "{{ kind }}" # Run every non-mutating Python source check. [group('workflows')] diff --git a/scripts/sanitizer.sh b/scripts/sanitizer.sh index 504c94776f..48ac60276a 100755 --- a/scripts/sanitizer.sh +++ b/scripts/sanitizer.sh @@ -28,6 +28,9 @@ fi source "${script_dir}/prepare-vcpkg.sh" prepare_reference_environment +clang --version +cmake --version +ninja --version prepare_vcpkg "${repo_root}" rm -rf -- "${build_dir}"