diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 46d1f11cc..487ee6e00 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/actions/setup-just/action.yml b/.github/actions/setup-just/action.yml index fb2fc053e..1a3136ade 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/_sanitizer.yml b/.github/workflows/_sanitizer.yml index 352b32993..2a3f84c81 100644 --- a/.github/workflows/_sanitizer.yml +++ b/.github/workflows/_sanitizer.yml @@ -71,18 +71,8 @@ jobs: - name: Set up Just uses: ./.github/actions/setup-just - - name: Resolve LLVM version - id: llvm-version - shell: bash - run: echo "version=$(just --evaluate llvm_version)" >> "$GITHUB_OUTPUT" - - - name: Set up Clang + - name: Set up pkgx uses: pkgxdev/setup@4d4ae97af87ccb39ab8be4e073dea697fef2c6f7 # v5.0.0 - with: - +: llvm.org@${{ steps.llvm-version.outputs.version }} - - - name: Report compiler version - run: clang --version - name: Restore artifacts or set up vcpkg uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11.6 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb83044b2..1aa0b2b81 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 }} @@ -135,26 +101,48 @@ 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 }} - - 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 >> $env:GITHUB_PATH + + - 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 + with: + vcpkgDirectory: ${{ github.workspace }}/.cache/vcpkg + + - 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/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 87e3cd2bc..0d8763cef 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/.github/workflows/doxygen.yml b/.github/workflows/doxygen.yml index 2dd2a0ab1..74d616b1f 100644 --- a/.github/workflows/doxygen.yml +++ b/.github/workflows/doxygen.yml @@ -20,9 +20,23 @@ 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: Set up pkgx + uses: pkgxdev/setup@4d4ae97af87ccb39ab8be4e073dea697fef2c6f7 # v5.0.0 + + - 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/.semgrepignore b/.semgrepignore new file mode 100644 index 000000000..9aab1819f --- /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 000000000..0d268d3f3 --- /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 b6912a486..4fe1111d8 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 a7da6bbb1..a245e978e 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 489d5f716..aefe49247 100644 --- a/Justfile +++ b/Justfile @@ -1,24 +1,30 @@ # 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" +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" 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 +32,31 @@ 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: + ./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 + {{ rng_benchmark_binary }} {{ draws }} + # Apply safe automatic formatting to C++/Python source and the Justfile. [group('workflows')] fix: _format-fix python-fix @@ -37,31 +68,92 @@ 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: - ./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')] -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-bootstrap-vcpkg --help >/dev/null + 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 +168,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 +189,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: @@ -122,6 +207,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 @@ -147,43 +241,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 +283,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 +308,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 f6ee23c32..6e55efc67 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 @@ -19,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) @@ -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,16 @@ 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. 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 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 @@ -157,11 +158,12 @@ 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 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 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 @@ -184,58 +186,78 @@ 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 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 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. 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 -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 `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. +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. @@ -298,6 +320,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 +331,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 +345,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,28 +367,47 @@ links (in 2+1 spacetime), and the timelike faces (in 3+1 spacetime). Online documentation is at . -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): +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). + +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. 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++ + +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 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 +scan the real source tree for false positives. The doctest executable can also be run directly: @@ -371,17 +415,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 +455,21 @@ 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 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). @@ -441,9 +490,9 @@ 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 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 +516,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 +544,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 000000000..38692ac4b --- /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 7160bbc69..688c33323 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 000000000..027b9a9ad --- /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 aac422d28..846de27ce 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. @@ -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 @@ -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/docs/metropolis-hastings.md b/docs/metropolis-hastings.md new file mode 100644 index 000000000..0ddd201ae --- /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 000000000..581026222 --- /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 ce0ead27f..c8fa7a3c8 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 72a91a565..771057c99 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 @@ -69,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 @@ -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 @@ -122,20 +133,21 @@ 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 - [[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,12 +157,31 @@ 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 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 @@ -168,18 +199,20 @@ 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 - [[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 +222,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 +254,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) { @@ -229,37 +275,48 @@ 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 - [[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 +339,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 +356,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 +368,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 +423,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 +442,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 +466,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 +477,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 +506,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(); @@ -542,32 +558,51 @@ 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 - [[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 +631,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 +683,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 +823,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; } } } @@ -832,18 +861,20 @@ 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 - [[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 +882,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 +920,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 5af196723..5fa0290e7 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" @@ -62,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 @@ -897,17 +900,21 @@ 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, 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 +950,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 +970,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) @@ -978,16 +987,21 @@ 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 - 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 +1024,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 +1251,40 @@ 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 + /// @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, 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. + /// @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, + 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/Formatters.hpp b/include/Formatters.hpp index ac34fcbb0..c4ed912f6 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 35734b0cf..62a118ced 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,38 @@ 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 + /// @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, - 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. + /// @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, + 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 7536fb783..cfa9b5ea3 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 99fdb2f30..328c839de 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 76e49cb9e..69dd92b1e 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 88f6795c0..efc93da90 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 d7757e0bb..c0aaabc2b 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 000000000..73b33550f --- /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 d5f614349..1f4c0a722 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 c1be346d0..6d5b647f7 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 @@ -328,89 +365,85 @@ namespace utilities return triangulation; } // read_file - /// @brief Roll a die with PCG - [[nodiscard]] inline auto die_roll() + /// @brief Roll a die using a caller-supplied + /// `std::uniform_random_bit_generator` + 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 + /// @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. See: - /// http://www.pcg-random.org/paper.html - /// for more details + /// 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) /// /// @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 /// 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), - std::forward(t_max_timeslice)); + return generate_random_int(generator, static_cast(1), + 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 ebbdd27cb..3a202fde1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,16 +1,45 @@ [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-bootstrap-vcpkg = "scripts.bootstrap_vcpkg:main" +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 +49,7 @@ experiments = [ [tool.ruff] line-length = 160 -src = ["src"] +src = ["scripts"] target-version = "py312" [tool.ruff.lint] @@ -103,8 +132,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 000000000..9374ca757 --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1 @@ +"""CDT++ Python support and experiment scripts.""" diff --git a/scripts/bootstrap-vcpkg.bat b/scripts/bootstrap-vcpkg.bat deleted file mode 100644 index 0fe228d80..000000000 --- a/scripts/bootstrap-vcpkg.bat +++ /dev/null @@ -1,136 +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 -) - -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\." ( - git -C "%VCPKG_DIR%" remote get-url origin 2>NUL ^| FINDSTR /X /C:"https://github.com/microsoft/vcpkg.git" >NUL - 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 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_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%" -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 }" -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 50225734e..000000000 --- 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 000000000..bb33281d2 --- /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 + +VCPKG_TOOL_RELEASE = "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") != 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 + 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 {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 ''}." + 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 c7731ee8d..7a80c4b87 100644 --- a/scripts/build.bat +++ b/scripts/build.bat @@ -12,21 +12,21 @@ 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%" 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 diff --git a/scripts/clang-tidy.sh b/scripts/clang-tidy.sh index 517fce7b5..1912893db 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/codeql-build.sh b/scripts/codeql-build.sh new file mode 100755 index 000000000..694f8bd4b --- /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/doxygen.sh b/scripts/doxygen.sh new file mode 100755 index 000000000..179d7063b --- /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/fast-build.bat b/scripts/fast-build.bat index e9c839031..a38788f0d 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/mnist_experiment.py b/scripts/mnist_experiment.py new file mode 100644 index 000000000..9a66f1804 --- /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 000000000..8d4f62a4a --- /dev/null +++ b/scripts/optimize_initialize.py @@ -0,0 +1,189 @@ +"""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 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: + """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)", + ) + 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) + + +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 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." + 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 _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 + 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)] + + for parameter_pair in parameters: + experiment = Experiment(api_key=api_key, project_name="cdt-plusplus") + try: + hyper_params = {"simplices": 12000, "foliations": 12, "seed": seed} + experiment.log_parameters(hyper_params) + init_radius = parameter_pair[0] + radial_factor = parameter_pair[1] + + 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. + 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() + finally: + experiment.end() + + +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, args.seed) + 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/pkgx-build.sh b/scripts/pkgx-build.sh index 8773cbb36..319b7ecc6 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 @@ -25,4 +27,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/prepare-vcpkg.sh b/scripts/prepare-vcpkg.sh index 9b60332db..263e67af3 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/release_check.py b/scripts/release_check.py new file mode 100755 index 000000000..1950848c2 --- /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/sanitizer.sh b/scripts/sanitizer.sh index cb21288b7..48ac60276 100755 --- a/scripts/sanitizer.sh +++ b/scripts/sanitizer.sh @@ -27,6 +27,10 @@ if [[ "$(uname -s)" != "Linux" ]]; then fi source "${script_dir}/prepare-vcpkg.sh" +prepare_reference_environment +clang --version +cmake --version +ninja --version prepare_vcpkg "${repo_root}" rm -rf -- "${build_dir}" @@ -36,5 +40,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/cdt" -s -n64 -t3 -a.6 -k1.1 -l.1 -p10 --no-output +"${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 --seed 92 diff --git a/scripts/semgrep_fixture_config.py b/scripts/semgrep_fixture_config.py new file mode 100755 index 000000000..0e79dce33 --- /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 000000000..c96d6b191 --- /dev/null +++ b/scripts/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for repository-owned support scripts.""" diff --git a/scripts/tests/test_bootstrap_vcpkg.py b/scripts/tests/test_bootstrap_vcpkg.py new file mode 100644 index 000000000..daf35591f --- /dev/null +++ b/scripts/tests/test_bootstrap_vcpkg.py @@ -0,0 +1,188 @@ +"""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.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") + 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.VCPKG_TOOL_RELEASE}-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) + expected_cache = (repository_root / ".cache" / "vcpkg").resolve() + self.assertIn(str(expected_cache), str(raised.exception)) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/tests/test_optimize_initialize.py b/scripts/tests/test_optimize_initialize.py new file mode 100644 index 000000000..0d3f456cb --- /dev/null +++ b/scripts/tests/test_optimize_initialize.py @@ -0,0 +1,80 @@ +"""Tests for the portable initializer optimization support script.""" + +from __future__ import annotations + +import argparse +import unittest +from pathlib import Path + +from scripts.optimize_initialize import _initializer_binary, _initializer_command, _parse_args, _parse_initializer_output, _parse_seed + + +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_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. +Timeslice 2 has 24 spacelike faces. +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) + + 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() diff --git a/scripts/tests/test_release_check.py b/scripts/tests/test_release_check.py new file mode 100644 index 000000000..4c859e390 --- /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 000000000..f2c74ba39 --- /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 c3de3d100..a9b5ae45b 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 f2a498490..6841a8fbf 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 3ac1b7df6..bb53db1e4 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 3a6c4181b..03c5a5cff 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 22b9e0a4a..000000000 --- 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 8e0fe5852..000000000 --- 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 aeaf09c43..922fb006c 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 795c65266..d9f64b528 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,36 @@ 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(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-function-ref-tests - cdt-foliated-triangulation-tests - cdt-utilities-tests - PROPERTIES LABELS "full-suite-duplicate" TIMEOUT 60) + 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 80f7c6663..81660e9b4 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,12 @@ 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); + CAPTURE(random.seed()); + auto const result = ergodic_moves::do_26_move(source, random); THEN("The move is rejected without changing the source") { @@ -548,7 +593,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 191e249b8..883be83f2 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 69b79bad8..a2b4e1f47 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 8b5dac466..1a8d46e4a 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 82505547a..451f108a6 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 003977d44..cb125570d 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 85fb27041..fd699ecfb 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 b45afa28b..f504cf282 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 000000000..b940601fd --- /dev/null +++ b/tests/Random_benchmark.cpp @@ -0,0 +1,87 @@ +/******************************************************************************* + 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 = 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' + << "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 000000000..9d5696e80 --- /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 000000000..0fba45118 --- /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 b12e20b50..bc39dd930 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,43 @@ 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); + } + AND_THEN( + "Downcasting to double collapses the distinction that MPFR preserves.") + { + 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 417ed7d9d..a03b1ff46 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 02725b30d..48cff8436 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 ed1b5fdfc..05fe339a3 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,16 @@ SCENARIO("Randomizing functions" * doctest::test_suite("utilities")) { WHEN("We generate six timeslices within the range.") { - 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); + cdt::Random generator{92}; + CAPTURE(generator.seed()); + // 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); + 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 +429,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 +445,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 01d406381..1182133fe 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 8af31242a..65c8871d0 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 000000000..a96385d09 --- /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 000000000..df8745f8e --- /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 000000000..5310a29e5 --- /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 8ff8650d5..95dc0ba23 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 e465afbde..340e062b4 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",