From 862637ae0be485f36816a346f3c7ede560371a82 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Fri, 24 Jul 2026 16:10:29 -0700 Subject: [PATCH 1/5] feat(concurrency)!: finalize parallel triangulation support - expose validated --threads limits in cdt and initialize - add the opt-in CGAL/oneTBB build, CI contract, and scaling records - persist thread limits and report source and toolchain provenance - finalize typed seeds, scoped enums, move APIs, and physical parameters - require CMake 4.4+ and enforce schema-12 configure diagnostics BREAKING CHANGE: CMake 4.4.0 or newer is required. The final 1.0 C++ API replaces legacy snake_case types and get_* accessors, separates RandomSeed from RandomStream, removes the MoveTracker template parameter, and requires validated PhysicalParameters for generalized S3 actions. Closes #88 --- .clang-format | 2 +- .github/CONTRIBUTING.md | 7 +- .github/workflows/ci.yml | 22 +- .github/workflows/codecov-upload.yml | 21 +- CMakeLists.txt | 37 +- CMakePresets.json | 54 ++- Justfile | 66 +++- README.md | 61 ++- cmake/RunCdtNoOutputTest.cmake | 20 +- cmake/RunPersistenceOutputTest.cmake | 25 ++ cmake/Version.hpp.in | 1 + docs/api-boundary.md | 44 ++- docs/cgal-integration.md | 16 +- docs/multithreading.md | 144 ++++++++ docs/reproducibility.md | 7 + include/Apply_move.hpp | 7 +- include/Ergodic_moves_3.hpp | 138 +++---- include/Foliated_triangulation.hpp | 139 +++---- include/Formatters.hpp | 2 +- include/Manifold.hpp | 20 +- include/Metropolis.hpp | 194 +++++----- include/Move_always.hpp | 27 +- include/Move_command.hpp | 66 ++-- include/Move_outcome.hpp | 15 +- include/Move_run.hpp | 19 +- include/Move_strategy.hpp | 9 +- include/Move_tracker.hpp | 79 ++-- include/Mpfr_value.hpp | 4 +- include/Random.hpp | 92 ++++- include/Runtime_config.hpp | 82 +++-- include/S3Action.hpp | 135 ++++--- include/Settings.hpp | 14 +- include/Torus_d.hpp | 2 +- include/Utilities.hpp | 207 ++++++----- scripts/build.bat | 23 +- scripts/build.sh | 23 +- scripts/pkgx-build.sh | 15 +- scripts/slurm.sh | 2 +- semgrep.yaml | 2 +- src/CMakeLists.txt | 15 +- src/cdt-viewer.cpp | 2 +- src/cdt.cpp | 26 +- src/initialize.cpp | 22 +- tests/Apply_move_test.cpp | 20 +- tests/Bistellar_flip_test.cpp | 76 ++-- tests/CGAL_benchmark.cpp | 134 +++++-- tests/CGAL_integration_test.cpp | 5 +- tests/CMakeLists.txt | 18 +- tests/Ergodic_moves_3_audit_test.cpp | 107 +++--- tests/Ergodic_moves_3_test.cpp | 38 +- tests/Foliated_triangulation_test.cpp | 94 +++-- tests/Function_ref_test.cpp | 14 +- tests/Geometry_test.cpp | 10 +- tests/Manifold_test.cpp | 83 ++--- tests/Metropolis_test.cpp | 352 +++++++++--------- tests/Move_always_test.cpp | 100 ++--- tests/Move_command_test.cpp | 164 ++++----- tests/Move_outcome_test.cpp | 97 +++-- tests/Move_run_test.cpp | 30 +- tests/Move_tracker_test.cpp | 68 ++-- tests/Parallel_triangulation_test.cpp | 469 ++++++++++++++++-------- tests/Public_api_consumer.cpp | 96 ++++- tests/Random_benchmark.cpp | 1 + tests/Random_test.cpp | 6 +- tests/Runtime_config_test.cpp | 66 +++- tests/S3Action_test.cpp | 180 ++++++--- tests/Settings_test.cpp | 4 +- tests/Tetrahedron_test.cpp | 8 +- tests/Torus_test.cpp | 62 +++- tests/Utilities_test.cpp | 119 +++--- tests/Vertex_test.cpp | 2 +- tests/semgrep/functional_boundaries.cpp | 6 +- 72 files changed, 2764 insertions(+), 1573 deletions(-) create mode 100644 docs/multithreading.md diff --git a/.clang-format b/.clang-format index 93126d6d7..7928b8aee 100644 --- a/.clang-format +++ b/.clang-format @@ -137,9 +137,9 @@ QualifierAlignment: Custom QualifierOrder: - static - inline + - constexpr - type - const - - constexpr - volatile - restrict RawStringFormats: diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 8d746b27e..695a8bf03 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -45,11 +45,14 @@ substantial implementation so its maintenance value and scope can be agreed upon just fix just check just ci + just build-parallel ``` `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 22-entry CTest suite: all 103 doctest unit scenarios - and 21 CLI integration tests. When changing C++ behavior, also run + its fixtures. `just ci` adds the supported build and complete 24-entry CTest suite: all 103 doctest unit scenarios + and 23 CLI integration tests. `just build-parallel` builds the distinct + CGAL/oneTBB configuration and runs the same scientific suite plus the + replayable parallel stress launcher, for 25 entries. 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. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e912f700..15f354f61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,7 +76,9 @@ jobs: shell: bash run: | { + echo "cmake=$(just --evaluate cmake_version)" echo "llvm=$(just --evaluate llvm_version)" + echo "ninja=$(just --evaluate ninja_version)" echo "uv=$(just --evaluate uv_version)" echo "zizmor=$(just --evaluate zizmor_version)" } >> "$GITHUB_OUTPUT" @@ -85,7 +87,18 @@ jobs: if: runner.os != 'Windows' uses: pkgxdev/setup@4d4ae97af87ccb39ab8be4e073dea697fef2c6f7 # v5.0.0 with: - +: llvm.org@${{ steps.tool-versions.outputs.llvm }} + +: | + llvm.org@${{ steps.tool-versions.outputs.llvm }} + cmake.org@${{ steps.tool-versions.outputs.cmake }} + ninja-build.org@${{ steps.tool-versions.outputs.ninja }} + + - name: Set up pinned CMake and Ninja on Windows + if: runner.os == 'Windows' + uses: pkgxdev/setup@4d4ae97af87ccb39ab8be4e073dea697fef2c6f7 # v5.0.0 + with: + +: | + cmake.org@${{ steps.tool-versions.outputs.cmake }} + ninja-build.org@${{ steps.tool-versions.outputs.ninja }} - name: Set up Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 @@ -133,6 +146,13 @@ jobs: CXX: ${{ matrix.cxx }} run: just ci + - name: Run the opt-in CGAL/oneTBB contract + env: + CC: ${{ matrix.cc }} + CDT_PKGX_COMPILER_PACKAGE: ${{ matrix.compiler_package }} + CXX: ${{ matrix.cxx }} + run: just build-parallel + build: name: build if: ${{ always() }} diff --git a/.github/workflows/codecov-upload.yml b/.github/workflows/codecov-upload.yml index a57a3ebe0..dc34f8aa2 100644 --- a/.github/workflows/codecov-upload.yml +++ b/.github/workflows/codecov-upload.yml @@ -54,12 +54,27 @@ jobs: - name: Set up Just uses: ./.github/actions/setup-just - - name: Setup GCC + - name: Resolve coverage tool versions + id: tool-versions + run: | + { + echo "cmake=$(just --evaluate cmake_version)" + echo "ninja=$(just --evaluate ninja_version)" + } >> "$GITHUB_OUTPUT" + + - name: Set up coverage toolchain uses: pkgxdev/setup@4d4ae97af87ccb39ab8be4e073dea697fef2c6f7 # v5.0.0 with: - +: gnu.org/gcc@16 + +: | + gnu.org/gcc@16 + cmake.org@${{ steps.tool-versions.outputs.cmake }} + ninja-build.org@${{ steps.tool-versions.outputs.ninja }} - - run: gcc --version + - name: Report coverage toolchain + run: | + cmake --version + ninja --version + gcc --version - name: Restore artifacts or setup vcpkg uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11.6 diff --git a/CMakeLists.txt b/CMakeLists.txt index a902b3f0c..4ad5a8220 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.21) +cmake_minimum_required(VERSION 4.4.0) # vcpkg settings must be set before project() if(DEFINED ENV{VCPKG_ROOT} AND NOT DEFINED CMAKE_TOOLCHAIN_FILE) @@ -30,6 +30,34 @@ project( # the product version used by executables and release metadata. set(CDT_VERSION_SUFFIX "-rc2") set(CDT_VERSION "${PROJECT_VERSION}${CDT_VERSION_SUFFIX}") +set(CDT_SOURCE_REVISION "unknown") +find_package(Git QUIET) +if(GIT_FOUND) + execute_process( + COMMAND "${GIT_EXECUTABLE}" rev-parse HEAD + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + RESULT_VARIABLE _git_revision_result + OUTPUT_VARIABLE _git_revision + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET) + if(_git_revision_result EQUAL 0) + set(CDT_SOURCE_REVISION "${_git_revision}") + execute_process( + COMMAND "${GIT_EXECUTABLE}" status --porcelain --untracked-files=no + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + RESULT_VARIABLE _git_status_result + OUTPUT_VARIABLE _git_status + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET) + if(_git_status_result EQUAL 0 AND NOT _git_status STREQUAL "") + string(APPEND CDT_SOURCE_REVISION "-dirty") + endif() + endif() + unset(_git_revision) + unset(_git_revision_result) + unset(_git_status) + unset(_git_status_result) +endif() # Project settings include(cmake/StandardProjectSettings.cmake) @@ -79,6 +107,13 @@ option(ENABLE_PARALLEL_TRIANGULATION option(ENABLE_DEPRECATION_ERRORS "Treat deprecated C++ and CGAL API use in project targets as an error" ON) +if(ENABLE_PARALLEL_TRIANGULATION AND ENABLE_SANITIZER_THREAD) + message( + FATAL_ERROR + "The pinned CGAL/oneTBB parallel path is not a supported ThreadSanitizer combination. Use the sequential tsan preset and the parallel asan preset." + ) +endif() + target_compile_definitions( project_options INTERFACE CDT_ENABLE_PARALLEL_TRIANGULATION=$) diff --git a/CMakePresets.json b/CMakePresets.json index 72919557d..bab114249 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -1,20 +1,40 @@ { - "version": 3, + "$schema": "https://raw.githubusercontent.com/Kitware/CMake/v4.4.0/Help/manual/presets/schema.json", + "version": 12, "cmakeMinimumRequired": { - "major": 3, - "minor": 21, + "major": 4, + "minor": 4, "patch": 0 }, "configurePresets": [ { "name": "common", "hidden": true, + "$comment": "Uninitialized-variable diagnostics are disabled because vcpkg and imported package configurations probe optional variables before defining them.", "generator": "Ninja", "binaryDir": "${sourceDir}/build", "cacheVariables": { "CMAKE_CXX_EXTENSIONS": false, "CMAKE_CXX_STANDARD": "23", "CMAKE_CXX_STANDARD_REQUIRED": true + }, + "warnings": { + "author": true, + "deprecated": true, + "experimental": true, + "installAbsoluteDestination": true, + "policy": true, + "uninitialized": false, + "unusedCli": true + }, + "errors": { + "author": true, + "deprecated": true, + "experimental": true, + "installAbsoluteDestination": true, + "policy": true, + "uninitialized": false, + "unusedCli": true } }, { @@ -47,6 +67,16 @@ "ENABLE_TESTING": true } }, + { + "name": "parallel", + "displayName": "Supported CGAL/oneTBB build", + "description": "Configure the opt-in CGAL parallel insertion and removal path with tests", + "inherits": "reference", + "binaryDir": "${sourceDir}/out/build/parallel", + "cacheVariables": { + "ENABLE_PARALLEL_TRIANGULATION": true + } + }, { "name": "debug", "displayName": "Local debug build", @@ -98,6 +128,11 @@ "name": "reference", "displayName": "Build the supported headless reference targets", "configurePreset": "reference" + }, + { + "name": "parallel", + "displayName": "Build the supported CGAL/oneTBB targets", + "configurePreset": "parallel" } ], "testPresets": [ @@ -113,6 +148,19 @@ "noTestsAction": "error", "stopOnFailure": false } + }, + { + "name": "parallel-smoke", + "displayName": "Run the CGAL/oneTBB invariant and stress suite", + "description": "Run the complete suite with the parallel TDS plus the replayable stress contract", + "configurePreset": "parallel", + "output": { + "outputOnFailure": true + }, + "execution": { + "noTestsAction": "error", + "stopOnFailure": false + } } ] } diff --git a/Justfile b/Justfile index 544dc2179..be911146e 100644 --- a/Justfile +++ b/Justfile @@ -10,6 +10,7 @@ git_cliff_version := "2.13.1" pinact_version := "4.1.0" pinact_module := "github.com/suzuki-shunsuke/pinact/v4/cmd/pinact@v" + pinact_version llvm_version := "22" +cmake_minimum_version := "4.4.0" cmake_version := "4.4.0" ninja_version := "1.13.2" doxygen_version := "1.17.0" @@ -18,12 +19,18 @@ 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" } cgal_benchmark_binary := if os_family() == "windows" { "out/build/reference/tests/CDT_cgal_benchmark.exe" } else { "out/build/reference/tests/CDT_cgal_benchmark" } +parallel_cgal_benchmark_binary := if os_family() == "windows" { "out/build/parallel/tests/CDT_cgal_benchmark.exe" } else { "out/build/parallel/tests/CDT_cgal_benchmark" } # Build the supported configuration through the repository build script. [group('workflows')] build: {{ if os_family() == "windows" { "cmd.exe //d //c 'scripts\\build.bat'" } else { "just _build-unix" } }} +# Build and test the opt-in CGAL/oneTBB configuration. +[group('workflows')] +build-parallel: + {{ if os_family() == "windows" { "cmd.exe //d //c 'scripts\\build.bat parallel'" } else { "just _build-parallel-unix" } }} + # Run fast, non-mutating local validation. [group('workflows')] check: _justfile-check _format-check _yaml-check _action-lint _zizmor _whitespace-check _cmake-check release-check python-check semgrep semgrep-test @@ -57,17 +64,41 @@ docs: # Build with GNU coverage instrumentation and generate LCOV and HTML reports. [group('workflows')] coverage: - ./scripts/coverage.sh + #!/usr/bin/env bash + set -euo pipefail + if command -v pkgx >/dev/null; then + exec pkgx \ + "+cmake.org@{{ cmake_version }}" \ + "+ninja-build.org@{{ ninja_version }}" \ + -- ./scripts/coverage.sh + fi + exec ./scripts/coverage.sh # Measure run-owned PCG sampling against the removed entropy-per-draw design. [group('workflows')] benchmark-rng draws='10000': build {{ rng_benchmark_binary }} {{ draws }} -# Measure seeded CGAL construction, repair, lookup, removal, and moves. +# Measure the deterministic sequential CGAL reference configuration. +[group('workflows')] +benchmark-cgal simplices='640' repetitions='5' moves='50' warmups='1': build + {{ cgal_benchmark_binary }} {{ simplices }} {{ repetitions }} {{ moves }} 1 {{ warmups }} + +# Record matched one-thread and increasing-thread CGAL/oneTBB scaling samples. [group('workflows')] -benchmark-cgal simplices='640' repetitions='5' moves='50': build - {{ cgal_benchmark_binary }} {{ simplices }} {{ repetitions }} {{ moves }} +benchmark-cgal-parallel threads='1 2 4' simplices='640' repetitions='5' moves='50' warmups='1': build-parallel + #!/usr/bin/env bash + set -euo pipefail + read -r -a thread_counts <<< {{ quote(threads) }} + [[ "${#thread_counts[@]}" -gt 0 ]] || { + echo "At least one thread count is required." >&2 + exit 2 + } + for thread_count in "${thread_counts[@]}"; do + {{ parallel_cgal_benchmark_binary }} \ + {{ quote(simplices) }} {{ quote(repetitions) }} {{ quote(moves) }} \ + "${thread_count}" {{ quote(warmups) }} + done # Apply safe automatic formatting to C++/Python source and the Justfile. [group('workflows')] @@ -254,6 +285,15 @@ _build-unix: fi exec ./scripts/build.sh +[private] +_build-parallel-unix: + #!/usr/bin/env bash + set -euo pipefail + if command -v pkgx >/dev/null; then + exec ./scripts/pkgx-build.sh --preset parallel + fi + exec ./scripts/build.sh parallel + [private] _codeql-phase phase: #!/usr/bin/env bash @@ -267,13 +307,25 @@ _codeql-phase phase: _cmake-check: #!/usr/bin/env bash set -euo pipefail + minimum="{{ cmake_minimum_version }}" + pinned="{{ cmake_version }}" if command -v cmake >/dev/null; then - exec cmake --list-presets=all >/dev/null + installed="$(cmake --version | awk 'NR == 1 { print $3 }')" + installed="${installed%%-*}" + IFS=. read -r installed_major installed_minor installed_patch <<< "$installed" + IFS=. read -r minimum_major minimum_minor minimum_patch <<< "$minimum" + if (( installed_major > minimum_major || + (installed_major == minimum_major && + (installed_minor > minimum_minor || + (installed_minor == minimum_minor && + installed_patch >= minimum_patch))) )); then + exec cmake --list-presets=all >/dev/null + fi fi if command -v pkgx >/dev/null; then - exec pkgx +cmake.org cmake --list-presets=all >/dev/null + exec pkgx "+cmake.org@$pinned" cmake --list-presets=all >/dev/null fi - echo "CMake is required; install it or install pkgx." >&2 + echo "CMake $minimum or newer is required; install it or install pkgx for the tested $pinned toolchain." >&2 exit 1 [private] diff --git a/README.md b/README.md index 144748c39..1427f7560 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,9 @@ The supported C++ namespace and per-header contract are recorded in the The exact CGAL version, kernel, TDS, metadata, lifetime, TBB, benchmark, and upgrade policies are recorded in the [CGAL 6.2 integration contract](docs/cgal-integration.md). +The opt-in operations, ownership and synchronization rules, replayable stress +inputs, sanitizer boundary, and matched scaling protocol are recorded in the +[multithreaded CGAL contract](docs/multithreading.md). [{fmt}] provides a safe and fast alternative to `iostream`. [spdlog] provides fast, multithreaded logging. [CometML] provides experiment tracking for the optional Python workflows. @@ -72,10 +75,10 @@ upgrade policies are recorded in the ### Regression-oracle scope The principal reason to preserve this implementation is its causality-filtering Delaunay construction path in -[`include/Foliated_triangulation.hpp`](include/Foliated_triangulation.hpp). `check_timevalues` classifies cells from -stored vertex time labels, `find_bad_vertex` selects a vertex responsible for an acausal local configuration, and -`fix_timevalues` removes offending vertices through CGAL so the cavity is retriangulated until the foliation contract -is satisfied. +[`include/Foliated_triangulation.hpp`](include/Foliated_triangulation.hpp). `find_invalid_timevalue_cells` classifies +cells from stored vertex time labels, `has_valid_timevalues` provides the predicate, `find_bad_vertex` selects a vertex +responsible for an acausal local configuration, and `fix_timevalues` removes offending vertices through CGAL so the +cavity is retriangulated until the foliation contract is satisfied. The deterministic doctest scenario **"Detecting and fixing problems with vertices and cells"** in [`tests/Foliated_triangulation_test.cpp`](tests/Foliated_triangulation_test.cpp) exercises this path with fixed points @@ -160,10 +163,13 @@ Windows development. ### Current reference-suite status With the pinned baseline, the reference configuration and build succeed on macOS with AppleClang. The cross-platform -`just build` command runs all 22 CTest entries through `scripts/build.sh` on Unix and `scripts/build.bat` on Windows: -one unit-test launcher containing 103 doctest scenarios and 21 CLI integration tests. The same `reference-smoke` preset +`just build` command runs all 24 CTest entries through `scripts/build.sh` on Unix and `scripts/build.bat` on Windows: +one unit-test launcher containing 103 doctest scenarios and 23 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. The parallel-enabled AddressSanitizer configuration adds one launcher containing four scenarios. +another doctest suite. The parallel-enabled AddressSanitizer and `parallel` +configurations add one launcher containing five scenarios, for 25 CTest +entries including the +replayable stress contract. ## Setup @@ -206,6 +212,7 @@ The repository-root [Justfile](Justfile) provides the same small command vocabul ```bash just check # Fast, non-mutating local checks +just build-parallel # Build and test the opt-in CGAL/oneTBB configuration 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 @@ -229,16 +236,23 @@ just python-fix # Apply safe Ruff fixes and formatting `check` covers repository-wide C++ formatting, Python formatting/lint/type checks, release metadata and citation fields, YAML, GitHub Actions syntax and security, whitespace, and CMake preset parsing. `ci` adds the pinact policy check and the supported build/test contract. 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: +`just docs-check`. The GitHub Actions Ubuntu GCC, Ubuntu Clang, macOS +AppleClang, and Windows MSVC jobs all run `just ci` followed by +`just build-parallel`. Windows continues to compile with native MSVC; the +locked Python environment supplies `clang-format` only as a source formatter. +Install the developer tools with Homebrew, use equivalent system packages, or +let pkgx supply the Unix environment ephemerally; pkgx remains optional. For +example: ```bash uv sync --locked --group dev -pkgx +just.systems@1.57.0 +git-scm.org +cmake.org +ninja-build.org +python.org +zizmor just check +pkgx +just.systems@1.57.0 +git-scm.org +cmake.org@4.4.0 +ninja-build.org +python.org +zizmor just check ``` +All configure paths require CMake 4.4.0 or newer. The Justfile owns the tested +4.4.0 toolchain pin: pkgx-backed recipes remain reproducible at that version, +while direct configure paths accept newer compatible CMake releases. + [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, Go, or a pkgx-provided Go fallback, then requires `yamllint`, `actionlint`, and `zizmor` to pass. The locked uv @@ -282,7 +296,9 @@ Windows; either platform-specific script can itself be run from any working dire 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. +preserve a compatible CMake cache and refresh it only when the selected vcpkg toolchain path changes. Direct script +invocations must expose CMake 4.4.0 or newer on `PATH`; the canonical pkgx-backed `just` recipes select the tested +4.4.0 toolchain automatically. ### Project Layout @@ -354,6 +370,7 @@ Usage:./cdt (--spherical | --toroidal) -n SIMPLICES -t TIMESLICES [--foliate FOLIATION SPACING] [--no-output] [--seed SEED] + [--threads THREADS] -k K --alpha ALPHA --lambda LAMBDA @@ -380,6 +397,8 @@ Options: files --seed arg Root random seed (default: operating-system entropy) + --threads arg (=1) Maximum worker threads for supported Delaunay + operations -a [ --alpha ] arg Negative squared geodesic length of 1-d timelike edges -k [ --k ] arg K = 1/(8*pi*G_newton) @@ -388,6 +407,12 @@ Options: -c [ --checkpoint ] arg (=10) Checkpoint every n passes ``` +`--threads` is a maximum concurrency limit for CGAL/oneTBB bulk Delaunay +operations. It defaults to 1. Zero and negative values are rejected. The +canonical reference build accepts only 1; values greater than 1 require the +`parallel` preset. This option does not parallelize Metropolis-Hastings, +Pachner moves, persistence, or concurrent access to one manifold. + The dimensionality of the spacetime is such that each slice of spacetime is `d-1`-dimensional, so setting `d=3` generates two spacelike dimensions and one timelike dimension, with a defined global time foliation. A @@ -436,10 +461,12 @@ subsequent releases. ## Testing Run `just build`; it selects `scripts/build.sh` on Unix or `scripts\build.bat` on Windows, builds the test target, and -executes all 22 CTest entries: one unit-test launcher containing 103 doctest scenarios and 21 executable integration -tests covering normal CLI use and invalid-boundary rejection. The parallel-enabled AddressSanitizer configuration adds -one launcher containing four scenarios labeled `unit`, `parallel`, and `configuration`. Every process-level test is -labeled `integration`, and invalid-input tests also carry the `cli-boundary` subcategory. Run `just ci` for the complete +executes all 24 CTest entries: one unit-test launcher containing 103 doctest scenarios and 23 executable integration +tests covering normal CLI use and invalid-boundary rejection. The parallel-enabled AddressSanitizer and `parallel` +configurations add one launcher containing five scenarios labeled `unit`, +`parallel`, and `configuration`, for 25 CTest entries. Every process-level test is labeled +`integration`, and 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 diff --git a/cmake/RunCdtNoOutputTest.cmake b/cmake/RunCdtNoOutputTest.cmake index b5c884b5c..e3c54fc67 100644 --- a/cmake/RunCdtNoOutputTest.cmake +++ b/cmake/RunCdtNoOutputTest.cmake @@ -29,15 +29,24 @@ 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" OR NOT help_output MATCHES "--seed") - message(FATAL_ERROR "cdt --help does not document --no-output and --seed") +if(NOT help_output MATCHES "--no-output" OR NOT help_output MATCHES "--seed" + OR NOT help_output MATCHES "--threads") + message( + FATAL_ERROR + "cdt --help does not document --no-output, --seed, and --threads") +endif() + +if(NOT DEFINED TEST_THREADS) + set(TEST_THREADS 1) endif() file(REMOVE_RECURSE "${normalized_test_directory}") file(MAKE_DIRECTORY "${normalized_test_directory}") execute_process( - COMMAND "${CDT_EXECUTABLE}" -s -n64 -t3 -a0.6 -k1.1 -l0.1 -p1 -c1 --no-output --seed 92 + COMMAND + "${CDT_EXECUTABLE}" -s -n64 -t3 -a0.6 -k1.1 -l0.1 -p1 -c1 + --no-output --seed 92 --threads "${TEST_THREADS}" WORKING_DIRECTORY "${normalized_test_directory}" RESULT_VARIABLE run_result OUTPUT_VARIABLE run_output @@ -51,6 +60,11 @@ 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() +if(NOT run_output MATCHES "Maximum Delaunay threads: ${TEST_THREADS}[\r\n]") + message( + FATAL_ERROR + "cdt did not report the requested thread limit:\n${run_output}") +endif() file(GLOB_RECURSE generated_files LIST_DIRECTORIES false "${normalized_test_directory}/*") if(generated_files) diff --git a/cmake/RunPersistenceOutputTest.cmake b/cmake/RunPersistenceOutputTest.cmake index a500ec5d6..93f6c039d 100644 --- a/cmake/RunPersistenceOutputTest.cmake +++ b/cmake/RunPersistenceOutputTest.cmake @@ -25,6 +25,10 @@ if(NOT DEFINED EXPECTED_ARTIFACT OR EXPECTED_ARTIFACT STREQUAL "") message(FATAL_ERROR "EXPECTED_ARTIFACT must name the persisted artifact kind") endif() +if(NOT DEFINED EXPECTED_THREADS OR NOT EXPECTED_THREADS MATCHES "^[1-9][0-9]*$") + message(FATAL_ERROR "EXPECTED_THREADS must be a positive integer") +endif() + file(REMOVE_RECURSE "${normalized_test_directory}") file(MAKE_DIRECTORY "${normalized_test_directory}") @@ -44,6 +48,17 @@ set(reported_seed "${CMAKE_MATCH_1}") if(DEFINED EXPECTED_SEED AND NOT reported_seed STREQUAL EXPECTED_SEED) message(FATAL_ERROR "Command reported seed ${reported_seed}, expected ${EXPECTED_SEED}") endif() +if(NOT run_output MATCHES "Maximum Delaunay threads: ([0-9]+)") + message( + FATAL_ERROR + "Command did not report its Delaunay thread limit:\n${run_output}") +endif() +set(reported_threads "${CMAKE_MATCH_1}") +if(NOT reported_threads STREQUAL EXPECTED_THREADS) + message( + FATAL_ERROR + "Command reported ${reported_threads} Delaunay threads, expected ${EXPECTED_THREADS}") +endif() file(GLOB payloads LIST_DIRECTORIES false "${normalized_test_directory}/*.off") file(GLOB manifests LIST_DIRECTORIES false "${normalized_test_directory}/*.off.meta") @@ -85,6 +100,16 @@ if(NOT metadata MATCHES "${required}") message(FATAL_ERROR "Manifest is missing '${required}':\n${metadata}") endif() endforeach() +string(REGEX MATCHALL "parallel[.]max_threads=[0-9]+" thread_fields "${metadata}") +list(LENGTH thread_fields thread_field_count) +if(NOT thread_field_count EQUAL 1 + OR NOT metadata + MATCHES + "(^|[\r\n])parallel[.]max_threads=${EXPECTED_THREADS}([\r\n]|$)") + message( + FATAL_ERROR + "Manifest does not contain exactly one expected Delaunay thread limit:\n${metadata}") +endif() if(EXPECTED_ARTIFACT STREQUAL "final-triangulation" AND (NOT metadata MATCHES "transition_trace.fnv1a64=" OR NOT metadata MATCHES "transition_trace.count=")) diff --git a/cmake/Version.hpp.in b/cmake/Version.hpp.in index 9af7d5389..6c39bd19e 100644 --- a/cmake/Version.hpp.in +++ b/cmake/Version.hpp.in @@ -16,6 +16,7 @@ namespace cdt { inline constexpr std::string_view VERSION{"@CDT_VERSION@"}; +inline constexpr std::string_view SOURCE_REVISION{"@CDT_SOURCE_REVISION@"}; inline constexpr std::string_view BUILD_COMPILER_ID{"@CMAKE_CXX_COMPILER_ID@"}; inline constexpr std::string_view BUILD_COMPILER_VERSION{ "@CMAKE_CXX_COMPILER_VERSION@"}; diff --git a/docs/api-boundary.md b/docs/api-boundary.md index 7162b2078..909e8d495 100644 --- a/docs/api-boundary.md +++ b/docs/api-boundary.md @@ -20,7 +20,7 @@ required by that library's customization contract. | --- | --- | --- | | `Apply_move.hpp` | `cdt::apply_move` | None | | `Ergodic_moves_3.hpp` | `cdt::ergodic_moves` aliases plus `null_move`, `do_*_move`, and `propose_*_move` | Every declaration in `cdt::ergodic_moves::detail`, including applicable-move preparation/execution, raw CGAL flips, cavity recognition, collection helpers, `bistellar_flip`, and `check_move` | -| `Foliated_triangulation.hpp` | Root CGAL interop aliases and `Cell_type`; construction, inspection, repair, and `FoliatedTriangulation` declarations in `cdt::foliated_triangulations` | Generic constraints and repair limits in `cdt::detail`; the component declarations are the supported advanced triangulation API | +| `Foliated_triangulation.hpp` | Root CGAL interop aliases, `CellType`, and `EdgeType`; construction, inspection, repair, and `FoliatedTriangulation` declarations in `cdt::foliated_triangulations` | Generic constraints and repair limits in `cdt::detail`; the component declarations are the supported advanced triangulation API | | `Formatters.hpp` | `fmt::formatter>` | Supported external-library customization point | | `Geometry.hpp` | `cdt::Geometry` and `cdt::Geometry_3` | None | | `Manifold.hpp` | `cdt::manifolds::make_causal_vertices`, `Manifold`, and `Manifold_3` | None | @@ -28,18 +28,18 @@ required by that library's customization contract. | `Move_always.hpp` | `cdt::MoveStrategy` move-always specialization and `cdt::MoveAlways_3` | Private members are implementation details | | `Move_command.hpp` | `cdt::MoveCommand` | Private queue and counter types are implementation details | | `Move_outcome.hpp` | `cdt::ergodic_moves::MoveFailure`, `MoveError`, `MoveResult`, `MoveOutcome`, and `outcome_from` | `format_as` is the supported `fmt`/`spdlog` customization hook for `MoveError` | -| `Move_strategy.hpp` | `cdt::Strategies` and `cdt::MoveStrategy` | None | -| `Move_tracker.hpp` | Move enumeration, counters, conversion, and sampling in `cdt::move_tracker` | None | +| `Move_strategy.hpp` | `cdt::MoveStrategyKind` and `cdt::MoveStrategy` | None | +| `Move_tracker.hpp` | `MoveType`, the non-generic `MoveTracker`, checked index conversion, and sampling in `cdt::move_tracker` | None | | `Mpfr_value.hpp` | Scoped MPFR value and operations in `cdt::mpfr_values` | None | | `Periodic_3_complex.hpp` | None | Legacy prototype in `cdt::experimental::periodic_complex`; unsupported and retained only for archival source access | | `Periodic_3_triangulations.hpp` | None | Legacy prototype in `cdt::experimental::periodic_triangulations`; unsupported and retained only for archival source access | -| `Random.hpp` | Random engine, seed/stream types, and named streams in `cdt` | None | +| `Random.hpp` | `Random`, distinct `RandomSeed` and `RandomStream` value types, and named streams in `cdt` | None | | `Runtime_config.hpp` | Validated configuration values and factories in `cdt::runtime_config` | Parsing helpers in `cdt::runtime_config::detail` | -| `S3Action.hpp` | Physical parameters and action functions in `cdt::s3_action` | None | +| `S3Action.hpp` | Validated `PhysicalParameters` and action functions in `cdt::s3_action` | Finite-coupling helpers in `cdt::s3_action::detail` | | `Settings.hpp` | Scalar types and named constants in `cdt` | Project-prefixed preprocessing exception described above | | `Torus_d.hpp` | None | Legacy prototype in `cdt::experimental::torus`; unsupported and retained only for archival source access | | `Triangulation_traits.hpp` | None | CGAL assembly traits in `cdt::detail`; supported aliases are published by `Foliated_triangulation.hpp` | -| `Utilities.hpp` | `cdt::topology_type` and persistence, reproducibility, random-distribution, logging, and conversion operations in `cdt::utilities` | Serialization and parsing machinery in `cdt::utilities::detail` | +| `Utilities.hpp` | `cdt::Topology` and persistence, reproducibility, random-distribution, logging, and conversion operations in `cdt::utilities` | Serialization and parsing machinery in `cdt::utilities::detail` | Tests that name `detail` are deliberate white-box tests for mutation failure-atomicity and malformed-handle rejection. Test access does not promote @@ -54,12 +54,32 @@ that they are usable downstream. See the ## Compatibility policy -This namespace normalization is the source-compatibility boundary for the -final 1.0 release. Repository production code, tests, and documented examples -were the only callers found for the pre-1.0 global and unrelated top-level -namespaces. Compatibility aliases would reintroduce precisely those global -names and would leave an indefinite archival burden, so no deprecated aliases -are provided. Downstream source must qualify project names through `cdt`. +The final 1.0 surface is the source-compatibility boundary. Repository +production code, tests, and documented examples were the only callers found +for the pre-1.0 declarations. Compatibility aliases would leave an indefinite +archival burden, so no deprecated aliases are provided. Downstream source must +qualify project names through `cdt`. + +The final pre-1.0 API pass made these intentional source changes: + +| Pre-1.0 spelling or contract | Final 1.0 contract | +| --- | --- | +| `topology_type`, `Artifact_kind`, `Cell_type`, `move_type`, `Strategies` | `Topology`, `ArtifactKind`, `CellType`, `MoveType`, `MoveStrategyKind` | +| Interchangeable `Random_seed` and `Random_stream` integer aliases | Distinct `RandomSeed` and `RandomStream` values with explicit construction and `.value()` at raw-integer boundaries | +| `MoveTracker` | Non-generic `MoveTracker`, because the counter has no manifold-dependent state | +| Unchecked `as_move(int)` with a `(4,4)` fallback | `move_from_index(std::size_t)` returning `std::optional` | +| Boolean edge classification and filtering | `EdgeType::TIMELIKE` or `EdgeType::SPACELIKE` | +| `check_timevalues()` returning an optional container | `has_valid_timevalues()` for the predicate and `find_invalid_timevalue_cells()` for diagnostics | +| `get_*()` accessors and `CalculateA1()` / `CalculateA2()` | Domain names such as `result()`, `geometry()`, `hastings_ratio()`, and `action_ratio()` | +| `S3_bulk_action*()` and `Gmpzf_to_double()` | `s3_bulk_action*()` and `gmpzf_to_double()` | + +Repair functions that mutate a triangulation now require a mutable +triangulation reference. `MoveCommand::result()` is ref-qualified: lvalues +borrow the stored result and rvalues transfer it. Unsupported +`MoveStrategyKind` specializations are incomplete rather than silently forming +an empty class. `PhysicalParameters` can only be created by its validating +factory, the generalized action consumes that proof value, and a Metropolis +strategy cannot be default-constructed without valid physical parameters. The project publishes no stable binary ABI: its library surface is templates and inline headers, and the CMake build produces executables rather than a diff --git a/docs/cgal-integration.md b/docs/cgal-integration.md index a741c2c83..4d59434ff 100644 --- a/docs/cgal-integration.md +++ b/docs/cgal-integration.md @@ -156,21 +156,23 @@ owner and pointer together. Returned snapshots are detached and operate sequentially unless a new owner explicitly attaches a compatible grid. The focused parallel test covers insertion, point/info association, copy, move, lock-zone refusal without mutation, range removal, wrapper transfer, and -post-donor lifetime. Issue #88 owns scaling and any broader multithreaded -simulation policy. +post-donor lifetime. The complete supported execution, determinism, +synchronization, sanitizer, stress, and scaling boundary is recorded in +[Multithreaded CGAL contract](multithreading.md). ## Reproducible performance baseline The benchmark is diagnostic rather than a pass/fail test: ```console -just benchmark-cgal 640 5 50 +just benchmark-cgal 640 5 50 1 ``` -The arguments are requested simplices, repetitions, and queued moves per -repetition. Input generation uses seed `102`. The report includes CGAL version, -TDS mode, generated and surviving topology counts, a checksum, and -minimum/median/maximum nanoseconds for: +The arguments are requested simplices, measured repetitions, queued moves per +repetition, and discarded warm-up repetitions. Input generation uses seed +`102`. The report includes source, toolchain, platform, dependency, thread, +fixture, and random-stream metadata; generated and surviving topology counts; +a checksum; every raw sample; and minimum/median/maximum nanoseconds for: - bulk point/info insertion; - foliation repair; diff --git a/docs/multithreading.md b/docs/multithreading.md new file mode 100644 index 000000000..4bcb311d4 --- /dev/null +++ b/docs/multithreading.md @@ -0,0 +1,144 @@ +# Multithreaded CGAL contract + +CDT++ retains one bounded multithreaded capability for the archived C++ +reference: CGAL may use oneTBB for bulk 3D Delaunay insertion and range +removal. The deterministic `reference` configuration remains the canonical +single-threaded correctness oracle. Enabling the parallel configuration does +not create a task runtime, parallelize Pachner moves, or make a +`FoliatedTriangulation` safe for concurrent member calls. + +## Build and test + +Run the supported opt-in configuration from a clean checkout with: + +```console +just build-parallel +``` + +This bootstraps the pinned vcpkg baseline, configures the `parallel` CMake +preset in `out/build/parallel`, builds every production and test target, and +runs the `parallel-smoke` preset. The complete ordinary suite is compiled with +`CGAL::Parallel_tag`; a separate launcher adds lock ownership, contention, +failure-atomicity, transfer, and replayable stress coverage. + +The equivalent CMake surface, when the pinned vcpkg toolchain is already +available, is: + +```console +cmake --preset parallel +cmake --build --preset parallel +ctest --preset parallel-smoke +``` + +The supported release matrix is the same C++23 matrix declared by the root +CMake configuration: Linux with GCC 13.3 or newer or Clang 22 or newer, macOS +with AppleClang 15 or newer, and Windows with MSVC 19.34 or newer. The primary +CI workflow runs `build-parallel` with Ubuntu GCC, Ubuntu Clang, macOS +AppleClang, and Windows MSVC. The Linux AddressSanitizer workflow supplies the +additional parallel sanitizer cell. Configuration fails when the selected +CGAL package does not provide `CGAL::TBB_support`; a compiler version alone is +not evidence that the dependency boundary is available. + +The stress scenario reports its replay inputs through doctest captures. +Override them to reproduce or expand a run: + +```console +CDT_PARALLEL_TEST_SEED=88 \ +CDT_PARALLEL_TEST_THREADS=4 \ +CDT_PARALLEL_TEST_ITERATIONS=16 \ +./out/build/parallel/tests/CDT_parallel_triangulation_test +``` + +## Runtime thread limit + +Both triangulation-producing commands expose the same resource contract: + +```console +./out/build/parallel/src/cdt ... --threads 4 +./out/build/parallel/src/initialize ... --threads 4 +``` + +`--threads N` sets oneTBB's maximum allowed parallelism for the lifetime of +the command, and each command reports and persists the selected value. The +default is 1 so an unqualified invocation remains single-threaded. Values +less than 1 are invalid. The `reference` build accepts only 1; larger values +require a `parallel` build. The value is a cap rather than a promise that +CGAL will schedule that many workers, especially for small triangulations. + +The scoped oneTBB control does not broaden the supported execution boundary: +only the eligible CGAL operations below can consume worker threads. + +## Supported execution boundary + +| Operation | Execution and ownership contract | +| --- | --- | +| Point/info range insertion | CGAL may schedule internal oneTBB work. The unpublished `Delaunay_state` owns the lock grid for the complete call. | +| Vertex-handle range removal | CGAL may schedule internal oneTBB work. No cached handle, iterator, or circulator is retained across removal. | +| Foliation repair | Classification is sequential. The invalid-vertex batch is removed through the supported CGAL range operation before caches are built. | +| Wrapper construction and caches | Sequential. A complete private triangulation is classified before publication. | +| Pachner moves and Metropolis-Hastings | Sequential copy/validate/swap transactions. Parallel builds preserve the same admissibility, action, probability, and counter contracts. | +| Persistence and snapshots | Sequential. Snapshots detach the non-owning lock pointer; persisted state is validated before atomic publication. | +| Concurrent wrapper access | Unsupported. Callers must externally serialize access to one `FoliatedTriangulation` or `Manifold`. Independent objects do not share topology or RNG state. | + +Every parallel triangulation has exactly one lock-grid owner. Copies allocate +and bind a new grid. Moves and swaps transfer the triangulation and owner +together. A handle belongs only to the triangulation that produced it; using a +handle after mutation or with a copy is unsupported. + +There is no public cancellation API. A supported CGAL range call runs to +completion inside its owning scope. Construction and repair happen on +unpublished state, so an exception destroys the complete candidate rather than +publishing partial topology. Published scientific mutations continue to use +private-copy validation followed by a non-throwing swap. + +`cdt::Random` remains thread-confined. The current parallel operations consume +no random draws inside worker tasks. Any future stochastic worker must receive +a unique, stable `root.split(worker_stream)` engine; sharing one mutable engine +is a data race and is unsupported. + +## Determinism and correctness + +The `reference` preset uses `CGAL::Sequential_tag` and remains the +step-by-step oracle. The `parallel` preset must preserve TDS validity, +foliation and causal classification, geometry counts, action values, move +semantics, cache invariants, and persistence round trips. Those contracts are +exercised by running the complete unit and integration suite under the +parallel TDS. + +oneTBB scheduling and CGAL's valid choices for cospherical input may change +fresh topology and the resulting Monte Carlo trajectory. Parallel runs +therefore promise invariant and distributional equivalence, not byte-identical +topology or trajectories. + +## Sanitizers + +The Linux `asan` preset enables the parallel TDS and exercises the parallel +contract under AddressSanitizer and UndefinedBehaviorSanitizer. The pinned +CGAL/oneTBB dependency build is not an authoritative ThreadSanitizer +combination, so CMake rejects `ENABLE_PARALLEL_TRIANGULATION=ON` together with +`ENABLE_SANITIZER_THREAD=ON`. The `tsan` preset continues to instrument the +repository-owned sequential path. This unsupported cell is explicit rather +than being reported as parallel TSan evidence. + +## Matched scaling record + +Correctness gates must pass before recording performance. Run the same +parallel binary at one and increasing thread limits: + +```console +just benchmark-cgal-parallel "1 2 4 8" 640 5 50 1 +``` + +The arguments are thread counts, requested simplices, measured repetitions, +queued moves, and discarded warm-up repetitions. Each record includes the +source revision and dirty marker, compiler and standard library, platform and +processor, dependency versions, logical and active thread counts, seed, +fixture and parameters, checksum, and every raw nanosecond sample. Run +`just benchmark-cgal` with the same workload for the canonical sequential-TDS +latency. + +Compare records only on the same machine, build profile, dependency baseline, +fixture, parameters, warm-up policy, and measurement protocol. The +`cdt-cgal-benchmark-v1` key/value record is the raw C++ input for issue #94's +language-neutral comparison manifest; #94 owns cross-language tables and +plots. A correctness failure invalidates the associated timing record. diff --git a/docs/reproducibility.md b/docs/reproducibility.md index 837cf1ec7..8c0212d2d 100644 --- a/docs/reproducibility.md +++ b/docs/reproducibility.md @@ -20,6 +20,12 @@ are invalid in Windows filenames. ./out/build/reference/src/initialize -s -n640 -t4 --seed 92 ``` +`--threads` is orthogonal to RNG ownership. It defaults to 1 and records the +maximum eligible Delaunay concurrency as `parallel.max_threads` in persisted +metadata. Parallel scheduling consumes no random draws; changing the thread +limit can nevertheless select another valid CGAL topology for cospherical +input, so the fresh-topology replay limitations below still apply. + 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 @@ -73,6 +79,7 @@ manifest. The manifest records: - requested and actual topology dimensions and counts; - foliation parameters and, for simulations, the action parameters and pass cadence; +- the maximum requested Delaunay thread count; - completed passes and the transition-trace fingerprint; - a canonical placement fingerprint derived from sorted finite vertices and their timeslices; diff --git a/include/Apply_move.hpp b/include/Apply_move.hpp index 74713384c..ea885f5bc 100644 --- a/include/Apply_move.hpp +++ b/include/Apply_move.hpp @@ -19,7 +19,6 @@ namespace cdt /** * \brief An applicative function similar to std::apply on a manifold * \tparam ManifoldType The type (topology, dimensionality) of manifold - * \tparam ExpectedType The result type of the move on the manifold * \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 @@ -27,8 +26,10 @@ namespace cdt * \return The expected or unexpected result in a std::expected */ template - auto constexpr apply_move(ManifoldType const& t_manifold, FunctionType t_move, - Arguments&&... arguments) -> decltype(auto) + [[nodiscard]] constexpr auto apply_move(ManifoldType const& t_manifold, + FunctionType t_move, + Arguments&&... arguments) + -> decltype(auto) { return std::invoke(t_move, t_manifold, std::forward(arguments)...); diff --git a/include/Ergodic_moves_3.hpp b/include/Ergodic_moves_3.hpp index 2d5875017..c02c7a449 100644 --- a/include/Ergodic_moves_3.hpp +++ b/include/Ergodic_moves_3.hpp @@ -162,8 +162,8 @@ namespace cdt::ergodic_moves { return m_bottom; } }; - [[nodiscard]] auto constexpr move_error( - MoveFailure const reason, move_tracker::move_type const move) noexcept + [[nodiscard]] constexpr auto move_error( + MoveFailure const reason, move_tracker::MoveType const move) noexcept -> std::unexpected { return std::unexpected{ @@ -203,8 +203,8 @@ namespace cdt::ergodic_moves [[nodiscard]] inline auto is_well_formed_edge( Edge_handle const& edge) noexcept -> bool { - static auto constexpr vertex_count = Int_precision{4}; - auto const valid_index = [](Int_precision const index) { + static constexpr auto vertex_count = Int_precision{4}; + auto const valid_index = [](Int_precision const index) { return index >= 0 && index < vertex_count; }; @@ -499,9 +499,9 @@ namespace cdt::ergodic_moves [[nodiscard]] inline auto get_vertices(Cell_container const& cells) -> Vertex_container; - [[nodiscard]] inline auto check_move(Manifold const& before, - Manifold const& after, - move_tracker::move_type const& move) + [[nodiscard]] inline auto check_move(Manifold const& before, + Manifold const& after, + move_tracker::MoveType const& move) -> bool; } // namespace detail @@ -522,7 +522,7 @@ namespace cdt::ergodic_moves Delaunay const& triangulation, Cell_handle const& candidate) -> std::expected { - using enum move_tracker::move_type; + using enum move_tracker::MoveType; if (candidate == nullptr || triangulation.dimension() != 3 || !triangulation.tds().is_cell(candidate)) { @@ -530,7 +530,7 @@ namespace cdt::ergodic_moves } if (!foliated_triangulations::is_cell_type_correct<3>(candidate) || foliated_triangulations::expected_cell_type<3>(candidate) != - Cell_type::TWO_TWO) + CellType::TWO_TWO) { return move_error(MoveFailure::CAUSAL_INVALIDITY, TWO_THREE); } @@ -549,8 +549,8 @@ namespace cdt::ergodic_moves auto const neighbor_type = foliated_triangulations::expected_cell_type<3>(neighbor); if (!foliated_triangulations::is_cell_type_correct<3>(neighbor) || - (neighbor_type != Cell_type::THREE_ONE && - neighbor_type != Cell_type::ONE_THREE)) + (neighbor_type != CellType::THREE_ONE && + neighbor_type != CellType::ONE_THREE)) { continue; } @@ -583,7 +583,7 @@ namespace cdt::ergodic_moves ApplicableTwoThreeMove const& move) -> Execution { - using enum move_tracker::move_type; + using enum move_tracker::MoveType; auto const cell = resolve_cell(triangulation, move.m_cell); auto const opposite = resolve_vertex(triangulation, move.m_opposite); if (!cell || !opposite) @@ -644,19 +644,19 @@ namespace cdt::ergodic_moves Delaunay triangulation{t_manifold.delaunay_snapshot()}; auto two_two = foliated_triangulations::filter_cells<3>( foliated_triangulations::collect_cells<3>(triangulation), - Cell_type::TWO_TWO); + CellType::TWO_TWO); detail::canonicalize(two_two); // Shuffle the container to create a random sequence of (2,2) cells std::ranges::shuffle(two_two, generator); if (two_two.empty()) { return detail::move_error(MoveFailure::NO_CANDIDATE, - move_tracker::move_type::TWO_THREE); + move_tracker::MoveType::TWO_THREE); } auto last_error = MoveError{.category = MoveFailure::NO_CANDIDATE, - .requested_move = move_tracker::move_type::TWO_THREE}; + .requested_move = move_tracker::MoveType::TWO_THREE}; for (auto const& cell : two_two) { auto const prepared = detail::prepare_two_three(triangulation, cell); @@ -686,12 +686,12 @@ namespace cdt::ergodic_moves auto triangulation = t_manifold.delaunay_snapshot(); auto two_two = foliated_triangulations::filter_cells<3>( foliated_triangulations::collect_cells<3>(triangulation), - Cell_type::TWO_TWO); + CellType::TWO_TWO); auto const candidate = detail::canonical_random_element(two_two, generator); if (!candidate) { return detail::move_error(MoveFailure::NO_CANDIDATE, - move_tracker::move_type::TWO_THREE); + move_tracker::MoveType::TWO_THREE); } auto const prepared = detail::prepare_two_three(triangulation, *candidate); if (!prepared) { return std::unexpected{prepared.error()}; } @@ -727,14 +727,14 @@ namespace cdt::ergodic_moves return false; } - auto const cell_type_count = [&](Cell_type const type) { + auto const cell_type_count = [&](CellType const type) { return std::ranges::count_if(*incident_cells, [&](auto const& cell) { return foliated_triangulations::expected_cell_type<3>(cell) == type; }); }; - auto const incident_31 = cell_type_count(Cell_type::THREE_ONE); - auto const incident_22 = cell_type_count(Cell_type::TWO_TWO); - auto const incident_13 = cell_type_count(Cell_type::ONE_THREE); + auto const incident_31 = cell_type_count(CellType::THREE_ONE); + auto const incident_22 = cell_type_count(CellType::TWO_TWO); + auto const incident_13 = cell_type_count(CellType::ONE_THREE); return incident_22 == 2 && ((incident_31 == 1 && incident_13 == 0) || (incident_31 == 0 && incident_13 == 1)); } @@ -748,7 +748,7 @@ namespace cdt::ergodic_moves Delaunay const& triangulation, Edge_handle const& candidate) -> std::expected { - using enum move_tracker::move_type; + using enum move_tracker::MoveType; if (!is_well_formed_edge(candidate) || triangulation.dimension() != 3 || !triangulation.tds().is_edge(candidate.first, candidate.second, candidate.third)) @@ -767,7 +767,7 @@ namespace cdt::ergodic_moves ApplicableThreeTwoMove const& move) -> Execution { - using enum move_tracker::move_type; + using enum move_tracker::MoveType; auto const edge = resolve_edge(triangulation, move.m_edge); if (!edge) { return move_error(MoveFailure::STALE_CANDIDATE, THREE_TWO); } if (!triangulation.flip(edge->first, edge->second, edge->third)) @@ -805,19 +805,20 @@ namespace cdt::ergodic_moves { Delaunay triangulation{t_manifold.delaunay_snapshot()}; auto timelike_edges = foliated_triangulations::filter_edges<3>( - foliated_triangulations::collect_edges<3>(triangulation), true); + foliated_triangulations::collect_edges<3>(triangulation), + EdgeType::TIMELIKE); detail::canonicalize(timelike_edges); // Shuffle the container to create a random sequence of edges std::ranges::shuffle(timelike_edges, generator); if (timelike_edges.empty()) { return detail::move_error(MoveFailure::NO_CANDIDATE, - move_tracker::move_type::THREE_TWO); + move_tracker::MoveType::THREE_TWO); } auto last_error = MoveError{.category = MoveFailure::NO_CANDIDATE, - .requested_move = move_tracker::move_type::THREE_TWO}; + .requested_move = move_tracker::MoveType::THREE_TWO}; for (auto const& edge : timelike_edges) { auto const prepared = detail::prepare_three_two(triangulation, edge); @@ -845,13 +846,14 @@ namespace cdt::ergodic_moves { auto triangulation = t_manifold.delaunay_snapshot(); auto timelike_edges = foliated_triangulations::filter_edges<3>( - foliated_triangulations::collect_edges<3>(triangulation), true); + foliated_triangulations::collect_edges<3>(triangulation), + EdgeType::TIMELIKE); auto const candidate = detail::canonical_random_element(timelike_edges, generator); if (!candidate) { return detail::move_error(MoveFailure::NO_CANDIDATE, - move_tracker::move_type::THREE_TWO); + move_tracker::MoveType::THREE_TWO); } auto const prepared = detail::prepare_three_two(triangulation, *candidate); if (!prepared) { return std::unexpected{prepared.error()}; } @@ -871,7 +873,7 @@ namespace cdt::ergodic_moves if (t_cell == nullptr || !foliated_triangulations::is_cell_type_correct<3>(t_cell) || foliated_triangulations::expected_cell_type<3>(t_cell) != - Cell_type::ONE_THREE) + CellType::ONE_THREE) { return std::nullopt; } @@ -881,7 +883,7 @@ namespace cdt::ergodic_moves auto const neighbor = t_cell->neighbor(i); if (foliated_triangulations::is_cell_type_correct<3>(neighbor) && foliated_triangulations::expected_cell_type<3>(neighbor) == - Cell_type::THREE_ONE) + CellType::THREE_ONE) { candidates.emplace_back(i); } @@ -906,7 +908,7 @@ namespace cdt::ergodic_moves Delaunay const& triangulation, Cell_handle const& candidate) -> std::expected { - using enum move_tracker::move_type; + using enum move_tracker::MoveType; if (candidate == nullptr || triangulation.dimension() != 3 || !triangulation.tds().is_cell(candidate)) { @@ -948,8 +950,8 @@ namespace cdt::ergodic_moves Delaunay& triangulation, ApplicableTwoSixMove const& move, Post_mutation_validator post_mutation_validator) -> Execution { - using enum move_tracker::move_type; - static auto constexpr incident_cell_count = std::size_t{6}; + using enum move_tracker::MoveType; + static constexpr auto incident_cell_count = std::size_t{6}; auto const bottom = resolve_cell(triangulation, move.bottom_points()); auto const opposite = resolve_vertex(triangulation, move.opposite_point()); @@ -1030,11 +1032,11 @@ namespace cdt::ergodic_moves Delaunay triangulation{t_manifold.delaunay_snapshot()}; auto one_three = foliated_triangulations::filter_cells<3>( foliated_triangulations::collect_cells<3>(triangulation), - Cell_type::ONE_THREE); + CellType::ONE_THREE); if (one_three.empty()) { return move_error(MoveFailure::NO_CANDIDATE, - move_tracker::move_type::TWO_SIX); + move_tracker::MoveType::TWO_SIX); } if (only_first_site) @@ -1044,7 +1046,7 @@ namespace cdt::ergodic_moves if (!candidate) { return move_error(MoveFailure::NO_CANDIDATE, - move_tracker::move_type::TWO_SIX); + move_tracker::MoveType::TWO_SIX); } one_three = {*candidate}; } @@ -1058,7 +1060,7 @@ namespace cdt::ergodic_moves auto last_error = MoveError{.category = MoveFailure::NO_CANDIDATE, - .requested_move = move_tracker::move_type::TWO_SIX}; + .requested_move = move_tracker::MoveType::TWO_SIX}; for (auto const& bottom : one_three) { auto const prepared = prepare_two_six(triangulation, bottom); @@ -1153,14 +1155,14 @@ namespace cdt::ergodic_moves if (triangulation.is_infinite(cell)) { return false; } } - auto const cell_type_count = [&](Cell_type const type) { + auto const cell_type_count = [&](CellType const type) { return std::ranges::count_if(incident_cells, [&](auto const& cell) { return foliated_triangulations::expected_cell_type<3>(cell) == type; }); }; - auto const incident_31 = cell_type_count(Cell_type::THREE_ONE); - auto const incident_22 = cell_type_count(Cell_type::TWO_TWO); - auto const incident_13 = cell_type_count(Cell_type::ONE_THREE); + auto const incident_31 = cell_type_count(CellType::THREE_ONE); + auto const incident_22 = cell_type_count(CellType::TWO_TWO); + auto const incident_13 = cell_type_count(CellType::ONE_THREE); // All cells should be causally classified and carry matching metadata. if (incident_13 + incident_22 + incident_31 != 6 || // NOLINT @@ -1182,7 +1184,7 @@ namespace cdt::ergodic_moves Delaunay const& triangulation, Vertex_handle const& candidate) -> std::expected { - using enum move_tracker::move_type; + using enum move_tracker::MoveType; if (candidate == nullptr || triangulation.dimension() != 3 || !triangulation.tds().is_vertex(candidate) || triangulation.is_infinite(candidate)) @@ -1218,7 +1220,7 @@ namespace cdt::ergodic_moves Generator& generator, Post_mutation_validator post_mutation_validator) -> std::expected { - using enum move_tracker::move_type; + using enum move_tracker::MoveType; Delaunay triangulation{source_triangulation}; auto const copied_candidate = foliated_triangulations::find_vertex<3>( triangulation, move.vertex_point()); @@ -1269,7 +1271,7 @@ namespace cdt::ergodic_moves for (auto const cell : triangulation.finite_cell_handles()) { auto const type = foliated_triangulations::expected_cell_type<3>(cell); - if (type == Cell_type::ACAUSAL || type == Cell_type::UNCLASSIFIED) + if (type == CellType::ACAUSAL || type == CellType::UNCLASSIFIED) { return move_error(MoveFailure::INVARIANT_VIOLATION, SIX_TWO); } @@ -1352,12 +1354,12 @@ namespace cdt::ergodic_moves if (vertices.empty()) { return detail::move_error(MoveFailure::NO_CANDIDATE, - move_tracker::move_type::SIX_TWO); + move_tracker::MoveType::SIX_TWO); } auto last_error = MoveError{.category = MoveFailure::NO_CANDIDATE, - .requested_move = move_tracker::move_type::SIX_TWO}; + .requested_move = move_tracker::MoveType::SIX_TWO}; for (auto const& vertex : vertices) { auto const prepared = detail::prepare_six_two(triangulation, vertex); @@ -1389,7 +1391,7 @@ namespace cdt::ergodic_moves if (!candidate) { return detail::move_error(MoveFailure::NO_CANDIDATE, - move_tracker::move_type::SIX_TWO); + move_tracker::MoveType::SIX_TWO); } auto const prepared = detail::prepare_six_two(triangulation, *candidate); if (!prepared) { return std::unexpected{prepared.error()}; } @@ -1435,14 +1437,14 @@ namespace cdt::ergodic_moves t_edge_candidate.first->vertex(t_edge_candidate.third)->info(); if (first_time != second_time) { return std::nullopt; } - auto const cell_type_count = [&](Cell_type const type) { + auto const cell_type_count = [&](CellType const type) { return std::ranges::count_if(*incident_cells, [&](auto const cell) { return foliated_triangulations::is_cell_type_correct<3>(cell) && foliated_triangulations::expected_cell_type<3>(cell) == type; }); }; - if (cell_type_count(Cell_type::THREE_ONE) == 2 && - cell_type_count(Cell_type::ONE_THREE) == 2) + if (cell_type_count(CellType::THREE_ONE) == 2 && + cell_type_count(CellType::ONE_THREE) == 2) { return incident_cells; } @@ -1456,7 +1458,7 @@ namespace cdt::ergodic_moves Delaunay const& triangulation, Edge_handle const& candidate) -> std::expected { - using enum move_tracker::move_type; + using enum move_tracker::MoveType; if (!is_well_formed_edge(candidate) || !triangulation.tds().is_edge(candidate.first, candidate.second, candidate.third)) @@ -1518,7 +1520,7 @@ namespace cdt::ergodic_moves Vertex_handle const& top, Vertex_handle const& bottom) -> std::expected { - using enum move_tracker::move_type; + using enum move_tracker::MoveType; if (!is_well_formed_edge(candidate) || top == nullptr || bottom == nullptr || !triangulation.tds().is_edge(candidate.first, candidate.second, @@ -1577,7 +1579,7 @@ namespace cdt::ergodic_moves Post_mutation_validator post_mutation_validator) -> std::expected { - using enum move_tracker::move_type; + using enum move_tracker::MoveType; Delaunay triangulation{source_triangulation}; auto const edge = resolve_edge(triangulation, move.edge_points()); auto const top = resolve_vertex(triangulation, move.top_point()); @@ -1607,9 +1609,9 @@ namespace cdt::ergodic_moves { return move_error(MoveFailure::STALE_CANDIDATE, FOUR_FOUR); } - auto constexpr cell_index_sum = 0 + 1 + 2 + 3; - auto const boundary_facet_index = cell_index_sum - pivot_from_1_index - - pivot_from_2_index - boundary_index; + constexpr auto cell_index_sum = 0 + 1 + 2 + 3; + auto const boundary_facet_index = cell_index_sum - pivot_from_1_index - + pivot_from_2_index - boundary_index; if (!triangulation.tds().flip( Delaunay::Facet{boundary_facet_cell, boundary_facet_index})) { @@ -1740,19 +1742,20 @@ namespace cdt::ergodic_moves { auto triangulation = t_manifold.delaunay_snapshot(); auto spacelike_edges = foliated_triangulations::filter_edges<3>( - foliated_triangulations::collect_edges<3>(triangulation), false); + foliated_triangulations::collect_edges<3>(triangulation), + EdgeType::SPACELIKE); detail::canonicalize(spacelike_edges); // Shuffle the container to pick a random sequence of edges to try std::ranges::shuffle(spacelike_edges, generator); if (spacelike_edges.empty()) { return detail::move_error(MoveFailure::NO_CANDIDATE, - move_tracker::move_type::FOUR_FOUR); + move_tracker::MoveType::FOUR_FOUR); } auto last_error = MoveError{.category = MoveFailure::NO_CANDIDATE, - .requested_move = move_tracker::move_type::FOUR_FOUR}; + .requested_move = move_tracker::MoveType::FOUR_FOUR}; for (auto const& edge : spacelike_edges) { auto const prepared = detail::prepare_four_four(triangulation, edge); @@ -1781,13 +1784,14 @@ namespace cdt::ergodic_moves { auto triangulation = t_manifold.delaunay_snapshot(); auto spacelike_edges = foliated_triangulations::filter_edges<3>( - foliated_triangulations::collect_edges<3>(triangulation), false); + foliated_triangulations::collect_edges<3>(triangulation), + EdgeType::SPACELIKE); auto const candidate = detail::canonical_random_element(spacelike_edges, generator); if (!candidate) { return detail::move_error(MoveFailure::NO_CANDIDATE, - move_tracker::move_type::FOUR_FOUR); + move_tracker::MoveType::FOUR_FOUR); } auto const prepared = detail::prepare_four_four(triangulation, *candidate); @@ -1812,7 +1816,7 @@ namespace cdt::ergodic_moves /// @return True if the move correctly changed the triangulation [[nodiscard]] inline auto detail::check_move( Manifold const& t_before, Manifold const& t_after, - move_tracker::move_type const& t_move) -> bool + move_tracker::MoveType const& t_move) -> bool { if (!t_after.is_structurally_correct() || !detail::same_configuration_value(t_after.initial_radius(), @@ -1825,7 +1829,7 @@ namespace cdt::ergodic_moves switch (t_move) { - case move_tracker::move_type::FOUR_FOUR: + case move_tracker::MoveType::FOUR_FOUR: return t_after.N3() == t_before.N3() && t_after.N3_31() == t_before.N3_31() && t_after.N3_22() == t_before.N3_22() && @@ -1836,7 +1840,7 @@ namespace cdt::ergodic_moves t_after.N0() == t_before.N0() && t_after.max_time() == t_before.max_time() && t_after.min_time() == t_before.min_time(); - case move_tracker::move_type::TWO_THREE: + case move_tracker::MoveType::TWO_THREE: return t_after.N3() == t_before.N3() + 1 && t_after.N3_31() == t_before.N3_31() && t_after.N3_22() == t_before.N3_22() + 1 && @@ -1848,7 +1852,7 @@ namespace cdt::ergodic_moves t_after.N0() == t_before.N0() && t_after.max_time() == t_before.max_time() && t_after.min_time() == t_before.min_time(); - case move_tracker::move_type::THREE_TWO: + case move_tracker::MoveType::THREE_TWO: return t_after.N3() == t_before.N3() - 1 && t_after.N3_31() == t_before.N3_31() && t_after.N3_22() == t_before.N3_22() - 1 && @@ -1860,7 +1864,7 @@ namespace cdt::ergodic_moves t_after.N0() == t_before.N0() && t_after.max_time() == t_before.max_time() && t_after.min_time() == t_before.min_time(); - case move_tracker::move_type::TWO_SIX: + case move_tracker::MoveType::TWO_SIX: return t_after.N3() == t_before.N3() + 4 && t_after.N3_31() == t_before.N3_31() + 2 && t_after.N3_22() == t_before.N3_22() && @@ -1872,7 +1876,7 @@ namespace cdt::ergodic_moves t_after.N0() == t_before.N0() + 1 && t_after.max_time() == t_before.max_time() && t_after.min_time() == t_before.min_time(); - case move_tracker::move_type::SIX_TWO: + case move_tracker::MoveType::SIX_TWO: return t_after.N3() == t_before.N3() - 4 && t_after.N3_31() == t_before.N3_31() - 2 && t_after.N3_22() == t_before.N3_22() && diff --git a/include/Foliated_triangulation.hpp b/include/Foliated_triangulation.hpp index c52fd3c25..61837c300 100644 --- a/include/Foliated_triangulation.hpp +++ b/include/Foliated_triangulation.hpp @@ -89,11 +89,11 @@ namespace cdt concept ConstForwardRange = std::ranges::forward_range< std::add_const_t>>; - inline int constexpr MAX_FIX_PASSES = 50; + inline constexpr int MAX_FIX_PASSES = 50; #if defined(CDT_ENABLE_PARALLEL_TRIANGULATION) && \ CDT_ENABLE_PARALLEL_TRIANGULATION - inline int constexpr LOCK_GRID_RESOLUTION = 50; + inline constexpr int LOCK_GRID_RESOLUTION = 50; [[nodiscard]] inline auto pad_locking_box(CGAL::Bbox_3 const& box) -> CGAL::Bbox_3 @@ -325,7 +325,7 @@ namespace cdt } // namespace detail /// (n,m) is number of vertices on (lower, higher) timeslice - enum class Cell_type + enum class CellType { // 3D simplices THREE_ONE = 31, // (3,1) @@ -334,6 +334,13 @@ namespace cdt ACAUSAL = 99, // The vertex timevalues differ by > 1 or are all equal UNCLASSIFIED = 0 // An error happened classifying cell }; + + /// @brief Causal classification of an edge by its endpoint timeslices. + enum class EdgeType + { + SPACELIKE, + TIMELIKE + }; } // namespace cdt namespace cdt::foliated_triangulations @@ -344,9 +351,9 @@ namespace cdt::foliated_triangulations /// @param timevalues The timevalue of each vertex /// @return A container of vertices that have an associated timevalue template - auto make_causal_vertices(std::span const> vertices, - std::span timevalues) - -> Causal_vertices_t + [[nodiscard]] auto make_causal_vertices( + std::span const> vertices, + std::span timevalues) -> Causal_vertices_t { if (vertices.size() != timevalues.size()) { @@ -436,7 +443,7 @@ namespace cdt::foliated_triangulations /// @tparam dimension The dimensionality of the simplices /// @returns True if timevalue of lhs is less than rhs template - auto constexpr compare_v_info = [](Vertex_handle_t const& lhs, + constexpr auto compare_v_info = [](Vertex_handle_t const& lhs, Vertex_handle_t const& rhs) { return lhs->info() < rhs->info(); }; @@ -476,10 +483,10 @@ namespace cdt::foliated_triangulations /// @brief Predicate to classify edge as timelike or spacelike /// @tparam dimension The dimensionality of the simplices /// @param t_edge The Edge_handle to classify - /// @returns True if timelike and false if spacelike + /// @returns The causal edge classification template [[nodiscard]] auto classify_edge(Edge_handle_t const& t_edge) - -> bool + -> EdgeType { #ifndef NDEBUG spdlog::debug("{} called.\n", CDT_PRETTY_FUNCTION); @@ -493,24 +500,24 @@ namespace cdt::foliated_triangulations time1, time2); #endif - return time1 != time2; + return time1 != time2 ? EdgeType::TIMELIKE : EdgeType::SPACELIKE; } // classify_edge /// @tparam dimension The dimensionality of the simplices /// @param t_edges The container of edges to filter - /// @param t_is_Timelike_pred The predicate to filter by - /// @returns A container of is_Timelike edges + /// @param edge_type The edge classification to retain + /// @returns A container of edges with the requested classification template [[nodiscard]] auto filter_edges( std::vector> const& t_edges, - bool t_is_Timelike_pred) -> std::vector> + EdgeType const edge_type) -> std::vector> { std::vector> filtered_edges; filtered_edges.reserve(t_edges.size()); - std::ranges::copy_if( - t_edges, std::back_inserter(filtered_edges), [&](auto const& edge) { - return t_is_Timelike_pred == classify_edge(edge); - }); + std::ranges::copy_if(t_edges, std::back_inserter(filtered_edges), + [&](auto const& edge) { + return edge_type == classify_edge(edge); + }); return filtered_edges; } // filter_edges @@ -521,7 +528,7 @@ namespace cdt::foliated_triangulations template [[nodiscard]] auto filter_cells( std::vector> const& t_cells, - Cell_type const& t_cell_type) -> std::vector> + CellType const& t_cell_type) -> std::vector> { std::vector> filtered_cells; filtered_cells.reserve(t_cells.size()); @@ -779,7 +786,7 @@ namespace cdt::foliated_triangulations mintime); spdlog::trace("--\n"); #endif - return Cell_type::ACAUSAL; + return CellType::ACAUSAL; } std::multiset const timevalues{vertex_timevalues.begin(), vertex_timevalues.end()}; @@ -787,9 +794,9 @@ namespace cdt::foliated_triangulations auto min_vertices = timevalues.count(mintime); // 3D simplices - if (max_vertices == 3 && min_vertices == 1) { return Cell_type::ONE_THREE; } - if (max_vertices == 2 && min_vertices == 2) { return Cell_type::TWO_TWO; } - if (max_vertices == 1 && min_vertices == 3) { return Cell_type::THREE_ONE; } + if (max_vertices == 3 && min_vertices == 1) { return CellType::ONE_THREE; } + if (max_vertices == 2 && min_vertices == 2) { return CellType::TWO_TWO; } + if (max_vertices == 1 && min_vertices == 3) { return CellType::THREE_ONE; } // If we got here, there's some kind of error #ifndef NDEBUG @@ -802,7 +809,7 @@ namespace cdt::foliated_triangulations max_vertices, min_vertices); spdlog::trace("--\n"); #endif - return Cell_type::UNCLASSIFIED; + return CellType::UNCLASSIFIED; } // expected_cell_type /// @brief Checks if a cell is classified correctly @@ -814,9 +821,9 @@ namespace cdt::foliated_triangulations Cell_handle_t const& t_cell) -> bool { auto cell_type = expected_cell_type(t_cell); - return cell_type != Cell_type::ACAUSAL && - cell_type != Cell_type::UNCLASSIFIED && - cell_type == static_cast(t_cell->info()); + return cell_type != CellType::ACAUSAL && + cell_type != CellType::UNCLASSIFIED && + cell_type == static_cast(t_cell->info()); } // is_cell_type_correct /// @brief Check all finite cells in the Delaunay triangulation @@ -856,8 +863,7 @@ namespace cdt::foliated_triangulations /// @param t_triangulation The Delaunay triangulation /// @return True if cells->info() was fixed template - [[nodiscard]] auto fix_cells(Delaunay_t const& t_triangulation) - -> bool + [[nodiscard]] auto fix_cells(Delaunay_t& t_triangulation) -> bool { auto incorrect_cells = find_incorrect_cells(t_triangulation); std::for_each( @@ -958,12 +964,12 @@ namespace cdt::foliated_triangulations /// @return Contiguous container with spacelike facets per timeslice template [[nodiscard]] auto collect_spacelike_facets(Container const& t_facets) - -> std::vector>> + -> std::vector>> { #ifndef NDEBUG spdlog::debug("{} called.\n", CDT_PRETTY_FUNCTION); #endif - using Volume_entry = std::pair>; + using Volume_entry = std::pair>; std::vector space_faces; if constexpr (std::ranges::sized_range) { @@ -1017,16 +1023,15 @@ namespace cdt::foliated_triangulations /// @param t_facets A container of facets /// @return Container with spacelike facets per timeslice template - [[nodiscard]] auto volume_per_timeslice(Container&& t_facets) - -> std::multimap> + [[nodiscard]] auto volume_per_timeslice(Container const& t_facets) + -> std::multimap> { - auto space_faces = - collect_spacelike_facets(std::forward(t_facets)); + auto space_faces = collect_spacelike_facets(t_facets); return {std::make_move_iterator(space_faces.begin()), std::make_move_iterator(space_faces.end())}; } // volume_per_timeslice - /// @brief Check cells for correct foliation + /// @brief Find cells whose vertex timevalues violate foliation /// @details The timevalues of the vertices of a cell differ by at most one /// and cannot all be the same. The first case would correspond to the /// cell (simplex) spanning more than one timeslice; the second would @@ -1039,24 +1044,28 @@ namespace cdt::foliated_triangulations /// fix_vertices() should be called before this function. /// @tparam dimension The dimensionality of the cells and triangulation /// @param t_triangulation The Delaunay triangulation - /// @return An optional container of invalid cells + /// @return A container of invalid cells; empty means the foliation is valid template - [[nodiscard]] auto check_timevalues( + [[nodiscard]] auto find_invalid_timevalue_cells( Delaunay_t const& t_triangulation) - -> std::optional>> + -> std::vector> { auto const& cells = collect_cells(t_triangulation); std::vector> invalid_cells; std::copy_if( cells.begin(), cells.end(), std::back_inserter(invalid_cells), [](auto const& cell) { - return expected_cell_type(cell) == Cell_type::ACAUSAL || - expected_cell_type(cell) == Cell_type::UNCLASSIFIED; + return expected_cell_type(cell) == CellType::ACAUSAL || + expected_cell_type(cell) == CellType::UNCLASSIFIED; }); - auto result = invalid_cells.empty() ? std::nullopt - : std::make_optional(invalid_cells); - return result; - } // check_timevalues + return invalid_cells; + } // find_invalid_timevalue_cells + + /// @brief Check whether all cell timevalues form a valid foliation. + template + [[nodiscard]] auto has_valid_timevalues( + Delaunay_t const& triangulation) -> bool + { return find_invalid_timevalue_cells(triangulation).empty(); } /// @brief Find the vertex that is causing a cell's foliation to be invalid /// @tparam dimension Dimensionality of the cell @@ -1101,14 +1110,15 @@ namespace cdt::foliated_triangulations -> bool { // Obtain a container of cells that are incorrectly foliated - if (auto invalid_cells = check_timevalues(t_triangulation); - invalid_cells) + auto invalid_cells = + find_invalid_timevalue_cells(t_triangulation); + if (!invalid_cells.empty()) { std::set> vertices_to_remove; // Transform the invalid cells into a set of vertices to remove // Reduction to unique vertices happens via the set container std::transform( - invalid_cells->begin(), invalid_cells->end(), + invalid_cells.begin(), invalid_cells.end(), std::inserter(vertices_to_remove, vertices_to_remove.begin()), find_bad_vertex); // Remove the vertices @@ -1282,7 +1292,7 @@ namespace cdt::foliated_triangulations } utilities::print_delaunay(triangulation); - assert(!check_timevalues(triangulation)); + assert(has_valid_timevalues(triangulation)); return std::move(state).into_detached_triangulation(); } // make_triangulation @@ -1424,12 +1434,12 @@ namespace cdt::foliated_triangulations return false; } - if (m_three_one != filter_cells<3>(m_cells, Cell_type::THREE_ONE) || - m_two_two != filter_cells<3>(m_cells, Cell_type::TWO_TWO) || - m_one_three != filter_cells<3>(m_cells, Cell_type::ONE_THREE) || + if (m_three_one != filter_cells<3>(m_cells, CellType::THREE_ONE) || + m_two_two != filter_cells<3>(m_cells, CellType::TWO_TWO) || + m_one_three != filter_cells<3>(m_cells, CellType::ONE_THREE) || m_spacelike_facets != cache_spacelike_facets(m_faces) || - m_timelike_edges != filter_edges<3>(m_edges, true) || - m_spacelike_edges != filter_edges<3>(m_edges, false)) + m_timelike_edges != filter_edges<3>(m_edges, EdgeType::TIMELIKE) || + m_spacelike_edges != filter_edges<3>(m_edges, EdgeType::SPACELIKE)) { return false; } @@ -1540,14 +1550,14 @@ namespace cdt::foliated_triangulations , m_foliation_spacing{foliation_spacing} , m_vertices{classify_vertices(collect_vertices<3>(triangulation()))} , m_cells{classify_cells(collect_cells<3>(triangulation()))} - , m_three_one{filter_cells<3>(m_cells, Cell_type::THREE_ONE)} - , m_two_two{filter_cells<3>(m_cells, Cell_type::TWO_TWO)} - , m_one_three{filter_cells<3>(m_cells, Cell_type::ONE_THREE)} + , m_three_one{filter_cells<3>(m_cells, CellType::THREE_ONE)} + , m_two_two{filter_cells<3>(m_cells, CellType::TWO_TWO)} + , m_one_three{filter_cells<3>(m_cells, CellType::ONE_THREE)} , m_faces{collect_faces()} , m_spacelike_facets{cache_spacelike_facets(m_faces)} , m_edges{foliated_triangulations::collect_edges<3>(triangulation())} - , m_timelike_edges{filter_edges<3>(m_edges, true)} - , m_spacelike_edges{filter_edges<3>(m_edges, false)} + , m_timelike_edges{filter_edges<3>(m_edges, EdgeType::TIMELIKE)} + , m_spacelike_edges{filter_edges<3>(m_edges, EdgeType::SPACELIKE)} , m_max_timevalue{find_max_timevalue<3>(std::span{m_vertices})} , m_min_timevalue{find_min_timevalue<3>(std::span{m_vertices})} {} @@ -1607,9 +1617,7 @@ namespace cdt::foliated_triangulations /// /// @return True if foliated correctly [[nodiscard]] auto is_foliated() const -> bool - { - return !static_cast(check_timevalues<3>(triangulation())); - } // is_foliated + { return has_valid_timevalues<3>(triangulation()); } // is_foliated /// @return True if the triangulation is Delaunay [[nodiscard]] auto is_delaunay() const -> bool @@ -1811,7 +1819,10 @@ namespace cdt::foliated_triangulations { for (auto const& edge : m_edges) { - if (classify_edge<3>(edge)) { fmt::print("==> timelike\n"); } + if (classify_edge<3>(edge) == EdgeType::TIMELIKE) + { + fmt::print("==> timelike\n"); + } else { fmt::print("==> spacelike\n"); @@ -1852,7 +1863,7 @@ namespace cdt::foliated_triangulations } // check_all_cells /// @brief Fix all cells in the triangulation - auto fix_cells() -> bool + [[nodiscard]] auto fix_cells() -> bool { Delaunay updated{triangulation()}; auto const changed = foliated_triangulations::fix_cells<3>(updated); @@ -1894,7 +1905,7 @@ namespace cdt::foliated_triangulations /// @brief Classify cells /// @param cells The container of simplices to classify - /// @return A container of simplices with Cell_type written to + /// @return A container of simplices with CellType written to /// cell->info() [[nodiscard]] auto classify_cells(Cell_container const& cells) const -> Cell_container diff --git a/include/Formatters.hpp b/include/Formatters.hpp index c4ed912f6..911e98072 100644 --- a/include/Formatters.hpp +++ b/include/Formatters.hpp @@ -26,7 +26,7 @@ namespace fmt struct formatter> { // Format specification handling - keeping it simple for now - auto constexpr parse(format_parse_context& ctx) -> decltype(ctx.begin()) + constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) { return ctx.begin(); } // Format the point as a string with coordinates diff --git a/include/Manifold.hpp b/include/Manifold.hpp index 240ae10ee..8e5542254 100644 --- a/include/Manifold.hpp +++ b/include/Manifold.hpp @@ -26,9 +26,9 @@ namespace cdt::manifolds /// @param timevalues A container of matching timevalues /// @return A container of Causal_vertices template - auto make_causal_vertices(std::span const> vertices, - std::span const timevalues) - -> Causal_vertices_t + [[nodiscard]] auto make_causal_vertices( + std::span const> vertices, + std::span const timevalues) -> Causal_vertices_t { return foliated_triangulations::make_causal_vertices(vertices, timevalues); @@ -63,19 +63,19 @@ namespace cdt::manifolds public: /// @brief Dimensionality of the manifold /// @details Used to determine the manifold dimension at compile-time - static int constexpr dimension = 3; + static constexpr int dimension = 3; /// @brief Topology of the manifold - static topology_type constexpr topology = topology_type::SPHERICAL; + static constexpr Topology topology = Topology::SPHERICAL; /// @brief Default dtor - ~Manifold() = default; + ~Manifold() = default; /// @brief Default ctor - Manifold() = default; + Manifold() = default; /// @brief Default copy ctor - Manifold(Manifold const& other) = default; + Manifold(Manifold const& other) = default; /// @brief Default copy assignment auto operator=(Manifold const& other) -> Manifold& = default; @@ -178,8 +178,8 @@ namespace cdt::manifolds { return m_triangulation.delaunay_snapshot(); } /// @returns A read-only reference to the Geometry - [[nodiscard]] auto get_geometry() const -> Geometry const& - { return m_geometry; } // get_geometry + [[nodiscard]] auto geometry() const -> Geometry const& + { return m_geometry; } // geometry /// @brief Forwarding to FoliatedTriangulation_3.is_foliated() /// @returns True if the Manifold triangulation is foliated diff --git a/include/Metropolis.hpp b/include/Metropolis.hpp index e250bb9ff..289f9eb78 100644 --- a/include/Metropolis.hpp +++ b/include/Metropolis.hpp @@ -49,9 +49,9 @@ namespace cdt /// @tparam ManifoldType The type of Manifold on which to apply the algorithm template requires(ManifoldType::dimension == 3) - class MoveStrategy + class MoveStrategy { - using Counter = move_tracker::MoveTracker; + using Counter = move_tracker::MoveTracker; using CommandResults = detail::MoveCommandResults; struct RunStatistics @@ -77,15 +77,8 @@ namespace cdt using PassResult = detail::MovePassResult; - /// @brief The length of the timelike edges - long double m_Alpha{}; - - /// @brief \f$K=\frac{1}{8\pi G_{N}}\f$. - long double m_K{}; - - /// @brief \f$\lambda=\frac{\Lambda}{8\pi G_{N}}\f$ where \f$\Lambda\f$ is - /// the cosmological constant - long double m_Lambda{}; + /// @brief Validated physical parameters used by every action evaluation + s3_action::PhysicalParameters m_parameters; /// @brief Positive pass and checkpoint cadence MoveRunCadence m_cadence; @@ -110,7 +103,7 @@ namespace cdt Int_precision m_checkpoint_events{}; static void record_transition( - RunStatistics& statistics, move_tracker::move_type const move, + RunStatistics& statistics, move_tracker::MoveType const move, ergodic_moves::MoveOutcome const outcome) noexcept { auto const append = [&statistics](std::uint8_t const value) { @@ -123,30 +116,25 @@ namespace cdt } public: - /// @brief Construct a default strategy on the named transition stream. - MoveStrategy() - { - m_reproducibility.seed = m_generator.seed(); - m_reproducibility.transition_stream = m_generator.stream(); - } + MoveStrategy() = delete; /// @brief Metropolis function object constructor /// @details Setup of runtime job parameters. - /// @param Alpha \f$\alpha\f$ is the timelike edge length. - /// @param K \f$k=\frac{1}{8\pi G_{Newton}}\f$ - /// @param Lambda \f$\lambda=k*\Lambda\f$ where \f$\Lambda\f$ is the + /// @param alpha \f$\alpha\f$ is the timelike edge length. + /// @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. /// @param passes Number of passes of ergodic moves on triangulation. /// @param checkpoint Print/write output for every n=checkpoint passes. /// @param write_files Whether checkpoints may write triangulation files. - [[maybe_unused]] MoveStrategy(long double const Alpha, long double const K, - long double const Lambda, + [[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 = true) - : MoveStrategy{Alpha, - K, - Lambda, + : MoveStrategy{alpha, + k, + lambda, passes, checkpoint, write_files, @@ -158,34 +146,30 @@ namespace cdt /// derived from the actual constructor arguments. Caller metadata supplies /// initialization and requested-state context only. [[maybe_unused]] MoveStrategy( - long double const Alpha, long double const K, long double const Lambda, + 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, std::optional reproducibility = std::nullopt) - : m_cadence{ - detail::parse_move_run_cadence(passes, checkpoint, "Metropolis")} + : m_parameters{s3_action::make_physical_parameters(alpha, k, lambda)} + , m_cadence{detail::parse_move_run_cadence(passes, checkpoint, + "Metropolis")} , m_write_files{write_files} , m_generator{std::move(random)} , m_reproducibility{ reproducibility.value_or(utilities::Reproducibility_metadata{ .seed = m_generator.seed(), - .alpha = Alpha, - .k = K, - .lambda = Lambda, + .alpha = alpha, + .k = k, + .lambda = lambda, .configured_passes = passes, .checkpoint_interval = checkpoint})} { - auto const parameters = - s3_action::make_physical_parameters(Alpha, K, Lambda); - m_Alpha = parameters.alpha; - m_K = parameters.k; - m_Lambda = parameters.lambda; m_reproducibility.seed = m_generator.seed(); m_reproducibility.transition_stream = m_generator.stream(); - m_reproducibility.alpha = m_Alpha; - m_reproducibility.k = m_K; - m_reproducibility.lambda = m_Lambda; + m_reproducibility.alpha = m_parameters.alpha(); + m_reproducibility.k = m_parameters.k(); + m_reproducibility.lambda = m_parameters.lambda(); m_reproducibility.configured_passes = m_cadence.passes(); m_reproducibility.checkpoint_interval = m_cadence.checkpoint(); #ifndef NDEBUG @@ -194,14 +178,14 @@ namespace cdt } /// @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, + 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) + cdt::RandomSeed const seed) : MoveStrategy{ - Alpha, - K, - Lambda, + alpha, + k, + lambda, passes, checkpoint, write_files, @@ -210,13 +194,13 @@ namespace cdt {} /// @returns The length of the timelike edge - [[nodiscard]] auto Alpha() const noexcept { return m_Alpha; } + [[nodiscard]] auto alpha() const noexcept { return m_parameters.alpha(); } /// @returns The normalized Newton's constant - [[nodiscard]] auto K() const noexcept { return m_K; } + [[nodiscard]] auto k() const noexcept { return m_parameters.k(); } /// @returns The normalized Cosmological constant - [[nodiscard]] auto Lambda() const noexcept { return m_Lambda; } + [[nodiscard]] auto lambda() const noexcept { return m_parameters.lambda(); } /// @returns The number of passes to make [[nodiscard]] auto passes() const noexcept { return m_cadence.passes(); } @@ -249,7 +233,7 @@ namespace cdt /// @brief Materialize output provenance for the supplied canonical state. [[nodiscard]] auto reproducibility_metadata( - ManifoldType const& manifold, utilities::Artifact_kind const artifact, + ManifoldType const& manifold, utilities::ArtifactKind const artifact, Int_precision const completed_passes) const -> utilities::Reproducibility_metadata { @@ -258,33 +242,40 @@ namespace cdt } /// @returns The container of trial moves - auto get_proposed() const { return m_run_statistics.proposed; } + [[nodiscard]] auto proposed() const noexcept -> Counter const& + { return m_run_statistics.proposed; } /// @returns The container of accepted moves - auto get_accepted() const { return m_run_statistics.accepted; } + [[nodiscard]] auto accepted() const noexcept -> Counter const& + { return m_run_statistics.accepted; } /// @returns The container of rejected moves - auto get_rejected() const { return m_run_statistics.rejected; } + [[nodiscard]] auto rejected() const noexcept -> Counter const& + { return m_run_statistics.rejected; } /// @returns The container of attempted moves - auto get_attempted() const { return m_command_results.attempted; } + [[nodiscard]] auto attempted() const noexcept -> Counter const& + { return m_command_results.attempted; } /// @returns The container of successful moves - auto get_succeeded() const { return m_command_results.succeeded; } + [[nodiscard]] auto succeeded() const noexcept -> Counter const& + { return m_command_results.succeeded; } /// @returns The container of failed moves - auto get_failed() const { return m_command_results.failed; } + [[nodiscard]] auto failed() const noexcept -> Counter const& + { return m_command_results.failed; } /// @returns The geometry used by the most recent acceptance decision - [[nodiscard]] auto get_geometry() const noexcept + [[nodiscard]] auto geometry() const noexcept -> Geometry const& { return m_run_statistics.geometry; } /// @returns The inverse Pachner move - [[nodiscard]] static auto constexpr reverse_move( - move_tracker::move_type const move) noexcept -> move_tracker::move_type + [[nodiscard]] static constexpr auto reverse_move( + move_tracker::MoveType const move) noexcept + -> std::optional { - using enum move_tracker::move_type; + using enum move_tracker::MoveType; switch (move) { case TWO_THREE: return THREE_TWO; @@ -293,15 +284,15 @@ namespace cdt case SIX_TWO: return TWO_SIX; case FOUR_FOUR: return FOUR_FOUR; } - return FOUR_FOUR; + return std::nullopt; } /// @returns The number of raw sites from which a move type is proposed - [[nodiscard]] static auto constexpr proposal_site_count( + [[nodiscard]] static constexpr auto proposal_site_count( Geometry const& geometry, - move_tracker::move_type const move) noexcept -> Int_precision + move_tracker::MoveType const move) noexcept -> Int_precision { - using enum move_tracker::move_type; + using enum move_tracker::MoveType; switch (move) { case TWO_THREE: return geometry.N3_22; @@ -319,7 +310,7 @@ namespace cdt /// self-transitions. [[nodiscard]] static auto proposal_probability( Geometry const& geometry, - move_tracker::move_type const move) -> mpfr_values::Value + move_tracker::MoveType const move) -> mpfr_values::Value { auto const sites = proposal_site_count(geometry, move); if (sites <= 0) { return mpfr_values::zero(); } @@ -333,13 +324,18 @@ namespace cdt /// @brief Calculate the Hastings reverse-to-forward proposal ratio /// @see [Metropolis-Hastings /// algorithm](../REFERENCES.md#metropolis-hastings-algorithm) - [[nodiscard]] static auto CalculateA1( + [[nodiscard]] static auto hastings_ratio( Geometry const& current, Geometry const& proposed, - move_tracker::move_type const move) -> mpfr_values::Value + move_tracker::MoveType const move) -> mpfr_values::Value { - auto const forward = proposal_probability(current, move); - auto const reverse = proposal_probability(proposed, reverse_move(move)); + auto const forward = proposal_probability(current, move); + auto const reverse_type = reverse_move(move); + if (!reverse_type) + { + throw std::invalid_argument{"Cannot reverse an unknown move type."}; + } + auto const reverse = proposal_probability(proposed, *reverse_type); if (mpfr_zero_p(forward.fr()) != 0 || mpfr_zero_p(reverse.fr()) != 0) { throw std::logic_error( @@ -351,17 +347,15 @@ namespace cdt /// @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( + [[nodiscard]] auto action_ratio( Geometry const& current, Geometry const& proposed) const -> mpfr_values::Value { - auto const current_action = - s3_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_action::S3_bulk_action(proposed.N1_TL, proposed.N3_31_13, - proposed.N3_22, m_Alpha, m_K, m_Lambda); + auto const current_action = s3_action::s3_bulk_action( + current.N1_TL, current.N3_31_13, current.N3_22, m_parameters); + auto const proposed_action = s3_action::s3_bulk_action( + proposed.N1_TL, proposed.N3_31_13, proposed.N3_22, m_parameters); return mpfr_values::exponential( mpfr_values::subtract(current_action, proposed_action)); } @@ -370,20 +364,21 @@ namespace cdt [[nodiscard]] auto acceptance_probability( Geometry const& current, Geometry const& proposed, - move_tracker::move_type const move) const -> mpfr_values::Value + move_tracker::MoveType const move) const -> mpfr_values::Value { - auto const ratio = mpfr_values::multiply( - CalculateA1(current, proposed, move), CalculateA2(current, proposed)); + auto const ratio = + mpfr_values::multiply(hastings_ratio(current, proposed, move), + action_ratio(current, proposed)); auto const one = mpfr_values::from_integer(1); return mpfr_cmp(ratio.fr(), one.fr()) < 0 ? ratio : one; } private: - [[nodiscard]] auto propose_candidate(ManifoldType const& current, - move_tracker::move_type const move) + [[nodiscard]] auto propose_candidate(ManifoldType const& current, + move_tracker::MoveType const move) -> ergodic_moves::MoveResult { - using enum move_tracker::move_type; + using enum move_tracker::MoveType; switch (move) { case TWO_THREE: @@ -405,7 +400,7 @@ namespace cdt } [[nodiscard]] auto make_reproducibility_metadata( - ManifoldType const& manifold, utilities::Artifact_kind const artifact, + ManifoldType const& manifold, utilities::ArtifactKind const artifact, Int_precision const completed_passes, RunStatistics const& statistics) const -> utilities::Reproducibility_metadata @@ -427,11 +422,11 @@ namespace cdt return metadata; } - auto attempt_transition(ManifoldType& current, - CommandResults& command_results, - RunStatistics& statistics, - move_tracker::move_type const move, - long double const trial_value) -> bool + auto attempt_transition(ManifoldType& current, + CommandResults& command_results, + RunStatistics& statistics, + move_tracker::MoveType const move, + long double const trial_value) -> bool { if (!std::isfinite(trial_value) || trial_value < 0.0L || trial_value > 1.0L) @@ -439,7 +434,7 @@ namespace cdt throw std::invalid_argument("MH trial value must lie in [0, 1]."); } - statistics.geometry = current.get_geometry(); + statistics.geometry = current.geometry(); ++statistics.proposed[move]; ++command_results.attempted[move]; @@ -463,11 +458,11 @@ namespace cdt ++command_results.succeeded[move]; auto const probability = acceptance_probability( - statistics.geometry, candidate->get_geometry(), move); + statistics.geometry, candidate->geometry(), move); if (mpfr_cmp_ld(probability.fr(), trial_value) >= 0) { swap(*candidate, current); - statistics.geometry = current.get_geometry(); + statistics.geometry = current.geometry(); ++statistics.accepted[move]; record_transition(statistics, move, ergodic_moves::MoveOutcome::METROPOLIS_ACCEPTED); @@ -571,9 +566,9 @@ namespace cdt /// @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 + [[nodiscard]] auto attempt_transition(ManifoldType& current, + move_tracker::MoveType const move, + long double const trial_value) -> bool { return attempt_transition(current, m_command_results, m_run_statistics, move, trial_value); @@ -581,19 +576,20 @@ namespace cdt /// @brief Initialize the cached action geometry from the canonical manifold void initialize(ManifoldType const& manifold) - { m_run_statistics.geometry = manifold.get_geometry(); } + { m_run_statistics.geometry = manifold.geometry(); } /// @brief Execute a fresh run while continuing the owned random stream. /// @details Counters, transition statistics, and checkpoint events are /// replaced only after the invocation completes. - auto operator()(ManifoldType const& t_manifold) -> ManifoldType + [[nodiscard]] auto operator()(ManifoldType const& t_manifold) + -> ManifoldType { #ifndef NDEBUG spdlog::debug("{} called.\n", CDT_PRETTY_FUNCTION); #endif auto initial_statistics = RunStatistics{}; - initial_statistics.geometry = t_manifold.get_geometry(); + initial_statistics.geometry = t_manifold.geometry(); auto result = detail::execute_move_run( t_manifold, std::move(initial_statistics), m_cadence, detail::MoveRunIdentity{.algorithm = "Metropolis-Hastings", @@ -614,7 +610,7 @@ namespace cdt Int_precision const pass_number) { utilities::write_file( current, make_reproducibility_metadata( - current, utilities::Artifact_kind::CHECKPOINT, + current, utilities::ArtifactKind::CHECKPOINT, pass_number, statistics)); }); @@ -630,7 +626,7 @@ namespace cdt }; // Metropolis using Metropolis_3 = - MoveStrategy; + MoveStrategy; } // namespace cdt #endif // INCLUDE_METROPOLIS_HPP_ diff --git a/include/Move_always.hpp b/include/Move_always.hpp index c3d0b221a..b2ded5aa3 100644 --- a/include/Move_always.hpp +++ b/include/Move_always.hpp @@ -26,7 +26,7 @@ namespace cdt /// @brief The Move Always algorithm template requires(ManifoldType::dimension == 3) - class MoveStrategy // NOLINT + class MoveStrategy // NOLINT { using CommandResults = detail::MoveCommandResults; using PassResult = detail::MovePassResult; @@ -59,7 +59,7 @@ namespace cdt command.execute(m_random); auto command_results = detail::consume_command_results(command); - return {.manifold = std::move(command.get_results()), + return {.manifold = std::move(command).result(), .command_results = std::move(command_results), .strategy_state = strategy_state}; } @@ -108,10 +108,10 @@ namespace cdt /// @param seed Root seed for the owned random stream. /// @param write_files Whether checkpoints may write triangulation files. /// @throws std::invalid_argument When either cadence value is nonpositive. - [[maybe_unused]] MoveStrategy(Int_precision const t_number_of_passes, - Int_precision const t_checkpoint, - cdt::Random_seed const seed, - bool const write_files = true) + [[maybe_unused]] MoveStrategy(Int_precision const t_number_of_passes, + Int_precision const t_checkpoint, + cdt::RandomSeed const seed, + bool const write_files = true) : MoveStrategy{t_number_of_passes, t_checkpoint, cdt::Random{seed}, write_files} {} @@ -153,20 +153,25 @@ namespace cdt [[nodiscard]] auto writes_files() const noexcept { return m_write_files; } /// @returns The MoveTracker of attempted moves - [[nodiscard]] auto get_attempted() const + [[nodiscard]] auto attempted() const noexcept + -> move_tracker::MoveTracker const& { return m_command_results.attempted; } /// @returns The MoveTracker of successful moves - [[nodiscard]] auto get_succeeded() const + [[nodiscard]] auto succeeded() const noexcept + -> move_tracker::MoveTracker const& { return m_command_results.succeeded; } /// @returns The array of failed moves - [[nodiscard]] auto get_failed() const { return m_command_results.failed; } + [[nodiscard]] auto failed() const noexcept + -> move_tracker::MoveTracker const& + { return m_command_results.failed; } /// @brief Execute a fresh run while continuing the owned random stream. /// @details Counters and checkpoint events are replaced only after the /// invocation completes. - auto operator()(ManifoldType const& t_manifold) -> ManifoldType + [[nodiscard]] auto operator()(ManifoldType const& t_manifold) + -> ManifoldType { #ifndef NDEBUG spdlog::debug("{} called.\n", CDT_PRETTY_FUNCTION); @@ -197,7 +202,7 @@ namespace cdt }; using MoveAlways_3 = - MoveStrategy; + MoveStrategy; } // namespace cdt #endif // INCLUDE_MOVE_ALWAYS_HPP_ diff --git a/include/Move_command.hpp b/include/Move_command.hpp index d47d71f05..e71209a58 100644 --- a/include/Move_command.hpp +++ b/include/Move_command.hpp @@ -18,14 +18,13 @@ namespace cdt { - template > - requires(ManifoldType::dimension == 3 && - std::same_as>) + template + requires(ManifoldType::dimension == 3) class MoveCommand { - using Queue = std::deque; - using Counter = move_tracker::MoveTracker; + using Queue = std::deque; + using Counter = move_tracker::MoveTracker; + using MoveResult = ergodic_moves::MoveResult; /** * \brief The manifold on which to perform moves @@ -71,33 +70,42 @@ namespace cdt {} /** - * \brief A read-only reference to the manifold + * \brief Access the result manifold without transferring ownership */ - auto get_const_results() const -> ManifoldType const& - { return m_manifold; } // get_const_results + [[nodiscard]] auto result() & noexcept -> ManifoldType& + { return m_manifold; } /** - * \brief Results of the moves invoked by MoveCommand + * \brief Access the result manifold without transferring ownership */ - [[nodiscard]] auto get_results() -> ManifoldType& { return m_manifold; } + [[nodiscard]] auto result() const& noexcept -> ManifoldType const& + { return m_manifold; } + + /** + * \brief Consume the result manifold + */ + [[nodiscard]] auto result() && noexcept -> ManifoldType + { return std::move(m_manifold); } + + auto result() const&& -> ManifoldType = delete; /** * \brief Attempted moves by MoveCommand */ - [[nodiscard]] auto get_attempted() const -> Counter const& - { return m_attempted; } // get_attempts + [[nodiscard]] auto attempted() const noexcept -> Counter const& + { return m_attempted; } /** * \brief Successful moves by MoveCommand */ - [[nodiscard]] auto get_succeeded() const -> Counter const& - { return m_succeeded; } // get_succeeded + [[nodiscard]] auto succeeded() const noexcept -> Counter const& + { return m_succeeded; } /** * \brief Failed moves by MoveCommand */ - [[nodiscard]] auto get_failed() const -> Counter const& - { return m_failed; } // get_errors + [[nodiscard]] auto failed() const noexcept -> Counter const& + { return m_failed; } /** * \brief Reset counters @@ -113,13 +121,13 @@ namespace cdt * \brief Push a Pachner move onto the move queue * \param t_move The move to add */ - void enqueue(move_tracker::move_type const t_move) + void enqueue(move_tracker::MoveType const t_move) { m_moves.push_front(t_move); } /** * \brief The number of moves on the queue */ - auto size() const { return m_moves.size(); } + [[nodiscard]] auto size() const noexcept { return m_moves.size(); } /** * \brief Execute all moves in the queue on the manifold @@ -129,15 +137,15 @@ namespace cdt { while (!m_moves.empty()) { - auto move_type = m_moves.back(); + auto const move = m_moves.back(); // Record attempted move - ++m_attempted[as_integer(move_type)]; + ++m_attempted[move]; auto result = - apply_random_move(std::as_const(m_manifold), move_type, generator); + apply_random_move(std::as_const(m_manifold), move, generator); auto outcome = result ? ergodic_moves::MoveOutcome::SUCCEEDED : ergodic_moves::outcome_from(result.error()); if (result && - !ergodic_moves::detail::check_move(m_manifold, *result, move_type)) + !ergodic_moves::detail::check_move(m_manifold, *result, move)) { outcome = ergodic_moves::MoveOutcome::EXECUTION_FAILED; spdlog::warn( @@ -147,11 +155,11 @@ namespace cdt if (outcome == ergodic_moves::MoveOutcome::SUCCEEDED) { swap(result.value(), m_manifold); - ++m_succeeded[as_integer(move_type)]; + ++m_succeeded[move]; } else { - ++m_failed[as_integer(move_type)]; + ++m_failed[move]; } // Remove move from queue m_moves.pop_back(); @@ -160,11 +168,11 @@ namespace cdt /// @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) -> ResultType + [[nodiscard]] static auto apply_random_move( + ManifoldType const& manifold, move_tracker::MoveType const move, + Generator& generator) -> MoveResult { - using enum move_tracker::move_type; + using enum move_tracker::MoveType; switch (move) { case TWO_THREE: return ergodic_moves::do_23_move(manifold, generator); diff --git a/include/Move_outcome.hpp b/include/Move_outcome.hpp index e0a01496a..2d2c722ee 100644 --- a/include/Move_outcome.hpp +++ b/include/Move_outcome.hpp @@ -35,20 +35,19 @@ namespace cdt::ergodic_moves /// derived at the presentation boundary rather than stored in the hot path. struct MoveError { - MoveFailure category; - move_tracker::move_type requested_move; + MoveFailure category; + move_tracker::MoveType requested_move; /// @returns The structured rejection or execution-failure category. - [[nodiscard]] auto constexpr reason() const noexcept -> MoveFailure + [[nodiscard]] constexpr auto reason() const noexcept -> MoveFailure { return category; } /// @returns The requested Pachner move. - [[nodiscard]] auto constexpr move() const noexcept - -> move_tracker::move_type + [[nodiscard]] constexpr auto move() const noexcept -> move_tracker::MoveType { return requested_move; } /// @returns A stable diagnostic for logs and command-line presentation. - [[nodiscard]] auto constexpr message() const noexcept -> std::string_view + [[nodiscard]] constexpr auto message() const noexcept -> std::string_view { using enum MoveFailure; switch (category) @@ -87,7 +86,7 @@ namespace cdt::ergodic_moves }; /// @brief Classify a structured move error for counter accounting. - [[nodiscard]] auto constexpr outcome_from(MoveError const error) noexcept + [[nodiscard]] constexpr auto outcome_from(MoveError const error) noexcept -> MoveOutcome { using enum MoveFailure; @@ -105,7 +104,7 @@ namespace cdt::ergodic_moves } /// @brief Enable direct formatting through fmt/spdlog. - [[nodiscard]] auto constexpr format_as(MoveError const error) noexcept + [[nodiscard]] constexpr auto format_as(MoveError const error) noexcept -> std::string_view { return error.message(); } } // namespace cdt::ergodic_moves diff --git a/include/Move_run.hpp b/include/Move_run.hpp index 579837a45..7a32fd950 100644 --- a/include/Move_run.hpp +++ b/include/Move_run.hpp @@ -74,10 +74,10 @@ namespace cdt } /// @returns The positive number of passes in a run. - [[nodiscard]] auto constexpr passes() const noexcept { return m_passes; } + [[nodiscard]] constexpr auto passes() const noexcept { return m_passes; } /// @returns The positive number of passes between checkpoint events. - [[nodiscard]] auto constexpr checkpoint() const noexcept + [[nodiscard]] constexpr auto checkpoint() const noexcept { return m_checkpoint; } }; @@ -109,7 +109,7 @@ namespace cdt requires(ManifoldType::dimension == 3) struct MoveCommandResults { - using Counter = move_tracker::MoveTracker; + using Counter = move_tracker::MoveTracker; Counter attempted; Counter succeeded; @@ -134,10 +134,9 @@ namespace cdt [[nodiscard]] auto consume_command_results(Command& command) -> MoveCommandResults { - MoveCommandResults result{ - .attempted = command.get_attempted(), - .succeeded = command.get_succeeded(), - .failed = command.get_failed()}; + MoveCommandResults result{.attempted = command.attempted(), + .succeeded = command.succeeded(), + .failed = command.failed()}; command.reset_counters(); return result; } @@ -164,9 +163,9 @@ namespace cdt /// @brief Stable identity displayed by the effectful run shell. struct MoveRunIdentity { - std::string_view algorithm; - cdt::Random_seed seed; - cdt::Random_stream stream; + std::string_view algorithm; + cdt::RandomSeed seed; + cdt::RandomStream stream; }; /// @brief Execute shared pass, accounting, checkpoint, and report cadence. diff --git a/include/Move_strategy.hpp b/include/Move_strategy.hpp index 674e0ab65..9d1339d9e 100644 --- a/include/Move_strategy.hpp +++ b/include/Move_strategy.hpp @@ -18,7 +18,7 @@ namespace cdt /** * \brief The algorithms available to make ergodic moves on triangulations */ - enum class Strategies + enum class MoveStrategyKind { MOVE_ALWAYS, METROPOLIS @@ -26,12 +26,11 @@ namespace cdt /** * \brief Select a move algorithm - * \tparam strategies The move algorithm to use + * \tparam strategy The move algorithm to use * \tparam ManifoldType The manifold to perform moves on */ - template - class MoveStrategy - {}; + template + class MoveStrategy; } // namespace cdt #endif // INCLUDE_MOVE_STRATEGY_HPP_ diff --git a/include/Move_tracker.hpp b/include/Move_tracker.hpp index b4bf438dc..7770fbb8b 100644 --- a/include/Move_tracker.hpp +++ b/include/Move_tracker.hpp @@ -15,20 +15,21 @@ #include #include #include +#include #include #include +#include #include "Settings.hpp" -#include "Utilities.hpp" namespace cdt::move_tracker { - inline Int_precision constexpr NUMBER_OF_3D_MOVES = 5; + inline constexpr std::size_t NUMBER_OF_3D_MOVES = 5; /** * \brief The types of 3D ergodic moves */ - enum class [[nodiscard("This contains data!")]] move_type + enum class [[nodiscard("This contains data!")]] MoveType { TWO_THREE = 0, THREE_TWO = 1, @@ -44,42 +45,42 @@ namespace cdt::move_tracker * \return The integer value of the enum */ template - auto as_integer(Enumeration value) -> std::underlying_type_t + requires std::is_enum_v + [[nodiscard]] constexpr auto as_integer(Enumeration const value) noexcept + -> std::underlying_type_t { return static_cast>(value); } // as_integer /** - * \brief Convert integer to move_type - * \param move_choice The move choice integer - * \return The move_type + * \brief Convert an integer index to MoveType + * \param move_choice The zero-based move index + * \return The MoveType, or std::nullopt when the index is out of range */ - inline auto as_move(int const move_choice) -> move_type + [[nodiscard]] constexpr auto move_from_index( + std::size_t const move_choice) noexcept -> std::optional { - if (move_choice == 0) { return move_type::TWO_THREE; } - if (move_choice == 1) { return move_type::THREE_TWO; } - if (move_choice == 2) { return move_type::TWO_SIX; } - if (move_choice == 3) { return move_type::SIX_TWO; } - return move_type::FOUR_FOUR; - } // as_move + using enum MoveType; + constexpr std::array moves{TWO_THREE, THREE_TWO, TWO_SIX, SIX_TWO, + FOUR_FOUR}; + if (move_choice >= moves.size()) { return std::nullopt; } + return moves[move_choice]; + } // move_from_index /// Generate a uniformly distributed 3D ergodic move from caller-owned RNG. template [[nodiscard]] inline auto generate_random_move_3(Generator& generator) - -> move_type + -> MoveType { std::uniform_int_distribution distribution{ 0, static_cast(NUMBER_OF_3D_MOVES - 1)}; auto const move_choice = distribution(generator); - return as_move(move_choice); + return *move_from_index(static_cast(move_choice)); } // generate_random_move_3 /** * \brief The data and methods to track ergodic moves - * \tparam ManifoldType The type of manifold on which moves are made */ - template - requires(ManifoldType::dimension == 3) class MoveTracker { using Container = std::array; @@ -91,14 +92,22 @@ namespace cdt::move_tracker * \brief Get a view of the moves * \return Read-only container of moves */ - auto moves_view() const { return std::span(moves); } + [[nodiscard]] auto moves_view() const noexcept { return std::span(moves); } /** * \brief The [] operator for MoveTracker * \param index The index of the element to be accessed * \return The number of moves at the index */ - auto operator[](gsl::index index) -> auto& + [[nodiscard]] auto operator[](gsl::index const index) -> auto& + { return gsl::at(moves, index); } // operator[] + + /** + * \brief The [] operator for a read-only MoveTracker + * \param index The index of the element to be accessed + * \return The number of moves at the index + */ + [[nodiscard]] auto operator[](gsl::index const index) const -> auto const& { return gsl::at(moves, index); } // operator[] /** @@ -106,7 +115,7 @@ namespace cdt::move_tracker * \param move The move type to be accessed * \return The number of moves of that type */ - auto operator[](move_type const move) -> auto& + [[nodiscard]] auto operator[](MoveType const move) -> auto& { return gsl::at(moves, as_integer(move)); } // operator[] /** @@ -114,7 +123,7 @@ namespace cdt::move_tracker * \param move The move type to be accessed * \return The number of moves of that type */ - auto operator[](move_type const move) const -> auto& + [[nodiscard]] auto operator[](MoveType const move) const -> auto const& { return gsl::at(moves, as_integer(move)); } // operator[] /** @@ -135,7 +144,7 @@ namespace cdt::move_tracker * \brief Total moves * \return The total number of moves in the MoveTracker */ - auto total() const noexcept + [[nodiscard]] auto total() const noexcept { return std::accumulate(moves.begin(), moves.end(), Int_precision{0}); } // total @@ -144,7 +153,7 @@ namespace cdt::move_tracker * \brief Container size * \return The size of the container of moves */ - auto size() const noexcept { return moves.size(); } + [[nodiscard]] auto size() const noexcept { return moves.size(); } // 3D @@ -152,61 +161,61 @@ namespace cdt::move_tracker * \brief Write access to (2,3) moves * \return Reference to number of (2,3) moves */ - auto two_three_moves() -> auto& { return gsl::at(moves, 0); } + [[nodiscard]] auto two_three_moves() -> auto& { return gsl::at(moves, 0); } /** * \brief Read access to (2,3) moves * \return Value of number of (2,3) moves */ - auto two_three_moves() const { return gsl::at(moves, 0); } + [[nodiscard]] auto two_three_moves() const { return gsl::at(moves, 0); } /** * \brief Write access to (3,2) moves * \return Reference to number of (3,2) moves */ - auto three_two_moves() -> auto& { return gsl::at(moves, 1); } + [[nodiscard]] auto three_two_moves() -> auto& { return gsl::at(moves, 1); } /** * \brief Read access to (3,2) moves * \return Value of number of (3,2) moves */ - auto three_two_moves() const { return gsl::at(moves, 1); } + [[nodiscard]] auto three_two_moves() const { return gsl::at(moves, 1); } /** * \brief Write access to (2,6) moves * \return Reference to number of (2,6) moves */ - auto two_six_moves() -> auto& { return gsl::at(moves, 2); } + [[nodiscard]] auto two_six_moves() -> auto& { return gsl::at(moves, 2); } /** * \brief Read access to (2,6) moves * \return Value of number of (2,6) moves */ - auto two_six_moves() const { return gsl::at(moves, 2); } + [[nodiscard]] auto two_six_moves() const { return gsl::at(moves, 2); } /** * \brief Write access to (6,2) moves * \return Reference to number of (6,2) moves */ - auto six_two_moves() -> auto& { return gsl::at(moves, 3); } + [[nodiscard]] auto six_two_moves() -> auto& { return gsl::at(moves, 3); } /** * \brief Read access to (6,2) moves * \return Value of number of (6,2) moves */ - auto six_two_moves() const { return gsl::at(moves, 3); } + [[nodiscard]] auto six_two_moves() const { return gsl::at(moves, 3); } /** * \brief Write access to (4,4) moves * \return Reference to number of (4,4) moves */ - auto four_four_moves() -> auto& { return gsl::at(moves, 4); } + [[nodiscard]] auto four_four_moves() -> auto& { return gsl::at(moves, 4); } /** * \brief Read access to (4,4) moves * \return Value of number of (4,4) moves */ - auto four_four_moves() const { return gsl::at(moves, 4); } + [[nodiscard]] auto four_four_moves() const { return gsl::at(moves, 4); } /// @brief Reset all moves counts to zero void reset() { moves.fill(0); } diff --git a/include/Mpfr_value.hpp b/include/Mpfr_value.hpp index bcff9c892..0d360503a 100644 --- a/include/Mpfr_value.hpp +++ b/include/Mpfr_value.hpp @@ -26,12 +26,12 @@ namespace cdt::mpfr_values /// Keeping one explicit policy at every arithmetic boundary prevents a /// caller from accidentally mixing directed roundings inside an action /// calculation. - inline auto constexpr rounding_mode = MPFR_RNDN; + inline constexpr auto rounding_mode = MPFR_RNDN; static_assert(std::is_nothrow_destructible_v, "MPFR values must release their resources without throwing."); - inline auto constexpr precision = + inline constexpr auto precision = static_cast(PRECISION); [[nodiscard]] inline auto zero() -> Value { return Value{0, precision}; } diff --git a/include/Random.hpp b/include/Random.hpp index 73b33550f..83de00db9 100644 --- a/include/Random.hpp +++ b/include/Random.hpp @@ -13,19 +13,69 @@ #include #include #include +#include #include #include "pcg_random.hpp" namespace cdt { - using Random_seed = std::uint64_t; - using Random_stream = std::uint64_t; + /// @brief Root entropy value used to reproduce a random run. + class RandomSeed final + { + std::uint64_t m_value{}; + + public: + constexpr RandomSeed() noexcept = default; + explicit constexpr RandomSeed(std::uint64_t const value) noexcept + : m_value{value} + {} + + [[nodiscard]] constexpr auto value() const noexcept -> std::uint64_t + { return m_value; } + + [[nodiscard]] auto operator==(RandomSeed const&) const noexcept + -> bool = default; + }; + + /// @brief PCG sequence selector derived from a root random seed. + class RandomStream final + { + std::uint64_t m_value{}; + + public: + constexpr RandomStream() noexcept = default; + explicit constexpr RandomStream(std::uint64_t const value) noexcept + : m_value{value} + {} + + [[nodiscard]] constexpr auto value() const noexcept -> std::uint64_t + { return m_value; } + + [[nodiscard]] auto operator==(RandomStream const&) const noexcept + -> bool = default; + }; + + [[nodiscard]] constexpr auto format_as(RandomSeed const seed) noexcept + -> std::uint64_t + { return seed.value(); } + + [[nodiscard]] constexpr auto format_as(RandomStream const stream) noexcept + -> std::uint64_t + { return stream.value(); } + + inline auto operator<<(std::ostream& output, RandomSeed const seed) + -> std::ostream& + { return output << seed.value(); } + + inline auto operator<<(std::ostream& output, RandomStream const stream) + -> std::ostream& + { return output << stream.value(); } namespace random_streams { - inline Random_stream constexpr initialization{0}; - inline Random_stream constexpr transitions{1}; + inline constexpr RandomStream initialization{0}; + inline constexpr RandomStream transitions{1}; } // namespace random_streams /// @brief A run-owned PCG engine with a recorded seed and stream identifier. @@ -45,48 +95,52 @@ namespace cdt using result_type = pcg64::result_type; private: - Random_seed m_seed{}; - Random_stream m_stream{}; + RandomSeed m_seed{}; + RandomStream m_stream{}; pcg64 m_engine; - [[nodiscard]] static auto entropy_seed() -> Random_seed + [[nodiscard]] static auto entropy_seed() -> RandomSeed { - std::random_device entropy; - std::uniform_int_distribution distribution{ - std::numeric_limits::min(), - std::numeric_limits::max()}; - return distribution(entropy); + std::random_device entropy; + std::uniform_int_distribution distribution{ + std::numeric_limits::min(), + std::numeric_limits::max()}; + return RandomSeed{distribution(entropy)}; } public: /// @brief Construct a root stream from operating-system entropy. Random() : Random{entropy_seed()} {} + /// @brief Construct a root stream from a raw seed value. + explicit Random(std::uint64_t const seed) : Random{RandomSeed{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} + explicit Random(RandomSeed const seed, + RandomStream const stream = RandomStream{}) + : m_seed{seed}, m_stream{stream}, m_engine{seed.value(), stream.value()} {} - [[nodiscard]] static auto constexpr min() noexcept -> result_type + [[nodiscard]] static constexpr auto min() noexcept -> result_type { return pcg64::min(); } - [[nodiscard]] static auto constexpr max() noexcept -> result_type + [[nodiscard]] static constexpr auto 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; } + [[nodiscard]] auto seed() const noexcept -> RandomSeed { return m_seed; } /// @returns The PCG stream selector used by this engine. - [[nodiscard]] auto stream() const noexcept -> Random_stream + [[nodiscard]] auto stream() const noexcept -> RandomStream { return m_stream; } /// @brief Create a fresh reproducible stream from the same root seed. - [[nodiscard]] auto split(Random_stream const stream) const -> Random + [[nodiscard]] auto split(RandomStream const stream) const -> Random { return Random{m_seed, stream}; } }; diff --git a/include/Runtime_config.hpp b/include/Runtime_config.hpp index 721286bfe..19dc54b52 100644 --- a/include/Runtime_config.hpp +++ b/include/Runtime_config.hpp @@ -11,6 +11,7 @@ #define CDT_PLUSPLUS_RUNTIME_CONFIG_HPP #include +#include #include #include #include @@ -29,24 +30,24 @@ namespace cdt::runtime_config friend auto make_triangulation(bool spherical, bool toroidal, long long simplices, long long timeslices, long long dimensions, double initial_radius, - double foliation_spacing, - cdt::Random_seed seed) -> Triangulation; - - topology_type m_topology; - Int_precision m_simplices; - Int_precision m_timeslices; - Int_precision m_dimensions; - double m_initial_radius; - double m_foliation_spacing; - cdt::Random_seed m_seed; - - explicit Triangulation(topology_type const topology, - Int_precision const simplices, - Int_precision const timeslices, - Int_precision const dimensions, - double const initial_radius, - double const foliation_spacing, - cdt::Random_seed const seed) noexcept + double foliation_spacing, + cdt::RandomSeed seed, long long threads) + -> Triangulation; + + Topology m_topology; + Int_precision m_simplices; + Int_precision m_timeslices; + Int_precision m_dimensions; + double m_initial_radius; + double m_foliation_spacing; + cdt::RandomSeed m_seed; + std::size_t m_threads; + + explicit Triangulation( + Topology const topology, Int_precision const simplices, + Int_precision const timeslices, Int_precision const dimensions, + double const initial_radius, double const foliation_spacing, + cdt::RandomSeed const seed, std::size_t const threads) noexcept : m_topology{topology} , m_simplices{simplices} , m_timeslices{timeslices} @@ -54,6 +55,7 @@ namespace cdt::runtime_config , m_initial_radius{initial_radius} , m_foliation_spacing{foliation_spacing} , m_seed{seed} + , m_threads{threads} {} public: @@ -63,7 +65,7 @@ namespace cdt::runtime_config auto operator=(Triangulation&&) noexcept -> Triangulation& = default; ~Triangulation() = default; - [[nodiscard]] auto topology() const noexcept -> topology_type + [[nodiscard]] auto topology() const noexcept -> Topology { return m_topology; } [[nodiscard]] auto simplices() const noexcept -> Int_precision @@ -81,8 +83,12 @@ namespace cdt::runtime_config [[nodiscard]] auto foliation_spacing() const noexcept -> double { return m_foliation_spacing; } - [[nodiscard]] auto seed() const noexcept -> cdt::Random_seed + [[nodiscard]] auto seed() const noexcept -> cdt::RandomSeed { return m_seed; } + + /// @returns The maximum oneTBB concurrency for eligible Delaunay work. + [[nodiscard]] auto threads() const noexcept -> std::size_t + { return m_threads; } }; /// Complete validated configuration for the Metropolis simulation. @@ -171,9 +177,32 @@ namespace cdt::runtime_config return static_cast(value); } + [[nodiscard]] inline auto checked_threads(long long const value) + -> std::size_t + { + if (value <= 0) + { + throw std::invalid_argument("Thread count must be positive."); + } + if (!std::in_range(value)) + { + throw std::out_of_range( + "Thread count exceeds the supported size range."); + } +#if !defined(CDT_ENABLE_PARALLEL_TRIANGULATION) || \ + !CDT_ENABLE_PARALLEL_TRIANGULATION + if (value != 1) + { + throw std::invalid_argument( + "This build supports only --threads 1; use the parallel preset " + "for larger values."); + } +#endif + return static_cast(value); + } + [[nodiscard]] inline auto select_topology(bool const spherical, - bool const toroidal) - -> topology_type + bool const toroidal) -> Topology { if (spherical == toroidal) { @@ -185,7 +214,7 @@ namespace cdt::runtime_config throw std::invalid_argument( "Toroidal triangulations are not yet supported."); } - return topology_type::SPHERICAL; + return Topology::SPHERICAL; } using GeneratedPopulation = utilities::Generated_population_bounds; @@ -231,7 +260,8 @@ namespace cdt::runtime_config bool const spherical, bool const toroidal, long long const simplices, long long const timeslices, long long const dimensions, double const initial_radius, double const foliation_spacing, - cdt::Random_seed const seed = 0) -> Triangulation + cdt::RandomSeed const seed = cdt::RandomSeed{}, + long long const threads = 1) -> Triangulation { auto const topology = detail::select_topology(spherical, toroidal); auto const checked_simplices = @@ -268,13 +298,15 @@ namespace cdt::runtime_config [[maybe_unused]] auto const population = detail::make_generated_population( checked_simplices, checked_timeslices, checked_initial_radius, checked_foliation_spacing); + auto const checked_threads = detail::checked_threads(threads); return Triangulation{topology, checked_simplices, checked_timeslices, checked_dimensions, checked_initial_radius, checked_foliation_spacing, - seed}; + seed, + checked_threads}; } /// Validate the complete simulation configuration. diff --git a/include/S3Action.hpp b/include/S3Action.hpp index a705e58a2..43037dde0 100644 --- a/include/S3Action.hpp +++ b/include/S3Action.hpp @@ -30,11 +30,26 @@ namespace cdt::s3_action { - struct PhysicalParameters + class PhysicalParameters { - long double alpha; - long double k; - long double lambda; + long double m_alpha; + long double m_k; + long double m_lambda; + + explicit constexpr PhysicalParameters(long double const alpha, + long double const k, + long double const lambda) noexcept + : m_alpha{alpha}, m_k{k}, m_lambda{lambda} + {} + + friend auto make_physical_parameters(long double alpha, long double k, + long double lambda) + -> PhysicalParameters; + + public: + [[nodiscard]] constexpr auto alpha() const noexcept { return m_alpha; } + [[nodiscard]] constexpr auto k() const noexcept { return m_k; } + [[nodiscard]] constexpr auto lambda() const noexcept { return m_lambda; } }; [[nodiscard]] inline auto make_physical_parameters(long double const alpha, @@ -50,19 +65,22 @@ namespace cdt::s3_action { throw std::domain_error("Alpha in 3D must be greater than 1/2."); } - return {alpha, k, lambda}; + return PhysicalParameters{alpha, k, lambda}; } - [[nodiscard]] inline auto make_finite_couplings(long double const k, - long double const lambda) - -> std::pair + namespace detail { - if (!std::isfinite(k) || !std::isfinite(lambda)) + [[nodiscard]] inline auto make_finite_couplings(long double const k, + long double const lambda) + -> std::pair { - throw std::invalid_argument("Physical parameters must be finite."); + if (!std::isfinite(k) || !std::isfinite(lambda)) + { + throw std::invalid_argument("Physical parameters must be finite."); + } + return {k, lambda}; } - return {k, lambda}; - } + } // namespace detail /// @brief Calculates S3 bulk action for \f$\alpha\f$=-1. /// /// This result is i* the action for Euclidean dynamically triangulated @@ -73,26 +91,26 @@ namespace cdt::s3_action /// \lambda\right)+N_3^{(2,2)}\left(7.386ik+0.118i\lambda\right) /// \equiv iS^3_{EDT}\f] /// - /// @param N1_TL \f$N_1^{TL}\f$ is the number of timelike links - /// @param N3_31_13 \f$N_3^{(3,1)}\f$ is the number of (3,1) and (1,3) + /// @param n1_tl_count \f$N_1^{TL}\f$ is the number of timelike links + /// @param n3_31_13_count \f$N_3^{(3,1)}\f$ is the number of (3,1) and (1,3) /// simplices - /// @param N3_22 \f$N_3^{(2,2)}\f$ is the number of (2,2) simplices - /// @param K \f$k=\frac{1}{8\pi G_{Newton}}\f$ - /// @param Lambda \f$\lambda=k*\Lambda\f$ where \f$\Lambda\f$ is the + /// @param n3_22_count \f$N_3^{(2,2)}\f$ is the number of (2,2) simplices + /// @param k_value \f$k=\frac{1}{8\pi G_{Newton}}\f$ + /// @param lambda_value \f$\lambda=k*\Lambda\f$ where \f$\Lambda\f$ is the /// Cosmological constant /// @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) - -> mpfr_values::Value + [[nodiscard]] inline auto s3_bulk_action_alpha_minus_one( + Int_precision const n1_tl_count, Int_precision const n3_31_13_count, + Int_precision const n3_22_count, long double const k_value, + long double const lambda_value) -> mpfr_values::Value { auto const [checked_k, checked_lambda] = - s3_action::make_finite_couplings(K, Lambda); + detail::make_finite_couplings(k_value, lambda_value); // Set input parameters and constants to MPFR equivalents - auto const n1_tl = mpfr_values::from_integer(N1_TL); - auto const n3_31 = mpfr_values::from_integer(N3_31_13); - auto const n3_22 = mpfr_values::from_integer(N3_22); + auto const n1_tl = mpfr_values::from_integer(n1_tl_count); + auto const n3_31 = mpfr_values::from_integer(n3_31_13_count); + auto const n3_22 = mpfr_values::from_integer(n3_22_count); auto const k = mpfr_values::from_long_double(checked_k); auto const lambda = mpfr_values::from_long_double(checked_lambda); auto const two = mpfr_values::from_integer(2); @@ -125,7 +143,7 @@ namespace cdt::s3_action auto const total = mpfr_values::add(r11, r12); // total = r11+r12 return total; - } // S3_bulk_action_alpha_minus_one() + } // s3_bulk_action_alpha_minus_one() /// @brief Calculates S3 bulk action for \f$\alpha\f$=1. /// @@ -134,26 +152,26 @@ namespace cdt::s3_action /// \f[S^{(3)}(\alpha=1)=2\pi k N_1^{TL}+N_3^{(3,1)}\left(-3.548k-0.167\lambda /// \right)+N_3^{(2,2)}\left(-5.355k-0.204\lambda\right)\f] /// - /// @param N1_TL \f$N_1^{TL}\f$ is the number of timelike links - /// @param N3_31_13 \f$N_3^{(3,1)}\f$ is the number of (3,1) and (1,3) + /// @param n1_tl_count \f$N_1^{TL}\f$ is the number of timelike links + /// @param n3_31_13_count \f$N_3^{(3,1)}\f$ is the number of (3,1) and (1,3) /// simplices - /// @param N3_22 \f$N_3^{(2,2)}\f$ is the number of (2,2) simplices - /// @param K \f$k=\frac{1}{8\pi G_{Newton}}\f$ - /// @param Lambda \f$\lambda=k*\Lambda\f$ where \f$\Lambda\f$ is the + /// @param n3_22_count \f$N_3^{(2,2)}\f$ is the number of (2,2) simplices + /// @param k_value \f$k=\frac{1}{8\pi G_{Newton}}\f$ + /// @param lambda_value \f$\lambda=k*\Lambda\f$ where \f$\Lambda\f$ is the /// Cosmological constant /// @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) - -> mpfr_values::Value + [[nodiscard]] inline auto s3_bulk_action_alpha_one( + Int_precision const n1_tl_count, Int_precision const n3_31_13_count, + Int_precision const n3_22_count, long double const k_value, + long double const lambda_value) -> mpfr_values::Value { auto const [checked_k, checked_lambda] = - s3_action::make_finite_couplings(K, Lambda); + detail::make_finite_couplings(k_value, lambda_value); // Set input parameters and constants to MPFR equivalents - auto const n1_tl = mpfr_values::from_integer(N1_TL); - auto const n3_31 = mpfr_values::from_integer(N3_31_13); - auto const n3_22 = mpfr_values::from_integer(N3_22); + auto const n1_tl = mpfr_values::from_integer(n1_tl_count); + auto const n3_31 = mpfr_values::from_integer(n3_31_13_count); + auto const n3_22 = mpfr_values::from_integer(n3_22_count); auto const k = mpfr_values::from_long_double(checked_k); auto const lambda = mpfr_values::from_long_double(checked_lambda); auto const two = mpfr_values::from_integer(2); @@ -187,7 +205,7 @@ namespace cdt::s3_action auto const total = mpfr_values::add(r11, r12); // total = r11+r12 return total; - } // S3_bulk_action_alpha_one() + } // 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 @@ -206,33 +224,28 @@ namespace cdt::s3_action /// {4\alpha +1}\right)-4k\sqrt{\alpha}\text{arccos}\left(\frac{-1}{4\alpha+1} /// \right)-\frac{\lambda}{12}\sqrt{4\alpha +2}\right]\f} /// - /// @param N1_TL \f$N_1^{TL}\f$ is the number of timelike links - /// @param N3_31_13 \f$N_3^{(3,1)}\f$ is the number of (3,1) and (1,3) + /// @param n1_tl_count \f$N_1^{TL}\f$ is the number of timelike links + /// @param n3_31_13_count \f$N_3^{(3,1)}\f$ is the number of (3,1) and (1,3) /// simplices - /// @param N3_22 \f$N_3^{(2,2)}\f$ is the number of (2,2) simplices - /// @param Alpha \f$\alpha\f$ is the timelike edge length - /// @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 + /// @param n3_22_count \f$N_3^{(2,2)}\f$ is the number of (2,2) simplices + /// @param parameters Validated physical parameters for the action /// @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 + [[nodiscard]] inline auto s3_bulk_action(Int_precision const n1_tl_count, + Int_precision const n3_31_13_count, + Int_precision const n3_22_count, + PhysicalParameters const& parameters) + -> mpfr_values::Value { - auto const parameters = - s3_action::make_physical_parameters(Alpha, K, Lambda); - // Set input parameters and constants to MPFR equivalents - auto const n1_tl = mpfr_values::from_integer(N1_TL); - auto const n3_31 = mpfr_values::from_integer(N3_31_13); - auto const n3_22 = mpfr_values::from_integer(N3_22); - auto const alpha = mpfr_values::from_long_double(parameters.alpha); - auto const k = mpfr_values::from_long_double(parameters.k); - auto const lambda = mpfr_values::from_long_double(parameters.lambda); + auto const n1_tl = mpfr_values::from_integer(n1_tl_count); + auto const n3_31 = mpfr_values::from_integer(n3_31_13_count); + auto const n3_22 = mpfr_values::from_integer(n3_22_count); + auto const alpha = mpfr_values::from_long_double(parameters.alpha()); + auto const k = mpfr_values::from_long_double(parameters.k()); + auto const lambda = mpfr_values::from_long_double(parameters.lambda()); auto const two = mpfr_values::from_integer(2); auto const pi = mpfr_values::pi(); auto const three = mpfr_values::from_integer(3); @@ -325,7 +338,7 @@ namespace cdt::s3_action auto const total = mpfr_values::add(r51, r52); // total = r51+r52 return total; - } // S3_bulk_action() + } // s3_bulk_action() #pragma GCC diagnostic pop diff --git a/include/Settings.hpp b/include/Settings.hpp index 8c37018ab..16946ac39 100644 --- a/include/Settings.hpp +++ b/include/Settings.hpp @@ -39,23 +39,23 @@ namespace cdt /// See Jonathan Boccara's C++ Pitfalls, January 2021 /// Sets the precision for MPFR. - inline Int_precision constexpr PRECISION = 256; + inline constexpr Int_precision PRECISION = 256; /// Default foliated triangulation spacings - inline double constexpr INITIAL_RADIUS = 1.0; - inline double constexpr FOLIATION_SPACING = 1.0; + inline constexpr double INITIAL_RADIUS = 1.0; + inline constexpr double FOLIATION_SPACING = 1.0; /// Sets epsilon values for floating point comparisons - inline double constexpr TOLERANCE = 0.01; + inline constexpr double TOLERANCE = 0.01; /// Depends on INITIAL_RADIUS and RADIAL_FACTOR - [[maybe_unused]] inline Int_precision constexpr GV_BOUNDING_BOX_SIZE = 100; + [[maybe_unused]] inline constexpr Int_precision GV_BOUNDING_BOX_SIZE = 100; /// Aligns data for ease of access on 64-bit CPUs at the expense of padding - inline int constexpr ALIGNMENT_64_BIT = 64; + inline constexpr int ALIGNMENT_64_BIT = 64; /// Except when we only need 32 bits - inline int constexpr ALIGNMENT_32_BIT = 32; + inline constexpr int ALIGNMENT_32_BIT = 32; } // namespace cdt #endif // INCLUDE_SETTINGS_HPP_ diff --git a/include/Torus_d.hpp b/include/Torus_d.hpp index 6850e0c54..27be46191 100644 --- a/include/Torus_d.hpp +++ b/include/Torus_d.hpp @@ -36,7 +36,7 @@ namespace cdt::experimental::torus inline void make_d_cube(std::vector& t_points, std::size_t t_number_of_points, int t_dimension) { - double constexpr size = 1.0; + constexpr double size = 1.0; fmt::print("Generating {} grid points in {}D\n", t_number_of_points, t_dimension); diff --git a/include/Utilities.hpp b/include/Utilities.hpp index 2f17d0b69..ba570f854 100644 --- a/include/Utilities.hpp +++ b/include/Utilities.hpp @@ -65,23 +65,23 @@ namespace cdt { - enum class topology_type + enum class Topology { TOROIDAL, SPHERICAL }; - /// @brief Convert topology_type to string output + /// @brief Convert Topology to string output /// @param t_os The output stream /// @param t_topology The topology /// @returns An output string of the topology - inline auto operator<<(std::ostream& t_os, topology_type const& t_topology) + inline auto operator<<(std::ostream& t_os, Topology const& t_topology) -> std::ostream& { switch (t_topology) { - case topology_type::SPHERICAL: return t_os << "spherical"; - case topology_type::TOROIDAL: return t_os << "toroidal"; + case Topology::SPHERICAL: return t_os << "spherical"; + case Topology::TOROIDAL: return t_os << "toroidal"; default: return t_os << "none"; } } // operator<< @@ -89,7 +89,7 @@ namespace cdt namespace cdt::utilities { - enum class Artifact_kind + enum class ArtifactKind { INITIAL_TRIANGULATION, CHECKPOINT, @@ -104,29 +104,30 @@ namespace cdt::utilities /// responsible for supplying truthful run configuration and RNG provenance. struct Reproducibility_metadata { - Artifact_kind artifact{Artifact_kind::FINAL_TRIANGULATION}; - cdt::Random_seed seed{}; - cdt::Random_stream initialization_stream{ + ArtifactKind artifact{ArtifactKind::FINAL_TRIANGULATION}; + cdt::RandomSeed seed{}; + cdt::RandomStream initialization_stream{ cdt::random_streams::initialization}; - cdt::Random_stream transition_stream{cdt::random_streams::transitions}; - topology_type topology{topology_type::SPHERICAL}; - Int_precision dimension{}; - Int_precision desired_simplices{}; - Int_precision desired_timeslices{}; - Int_precision actual_vertices{}; - Int_precision actual_edges{}; - Int_precision actual_faces{}; - Int_precision actual_simplices{}; - Int_precision minimum_timeslice{}; - Int_precision maximum_timeslice{}; - double initial_radius{}; - double foliation_spacing{}; + cdt::RandomStream transition_stream{cdt::random_streams::transitions}; + Topology topology{Topology::SPHERICAL}; + Int_precision dimension{}; + Int_precision desired_simplices{}; + Int_precision desired_timeslices{}; + Int_precision actual_vertices{}; + Int_precision actual_edges{}; + Int_precision actual_faces{}; + Int_precision actual_simplices{}; + Int_precision minimum_timeslice{}; + Int_precision maximum_timeslice{}; + double initial_radius{}; + double foliation_spacing{}; std::optional alpha; std::optional k; std::optional lambda; std::optional configured_passes; std::optional checkpoint_interval; std::optional completed_passes; + std::optional max_threads; std::optional transition_trace; std::optional transition_count; std::optional placement_fingerprint; @@ -143,7 +144,7 @@ namespace cdt::utilities namespace detail { - inline std::string_view constexpr CAUSAL_INFO_HEADER{ + inline constexpr std::string_view CAUSAL_INFO_HEADER{ "cdt-plusplus-causal-info-v1"}; struct Payload_integrity @@ -152,15 +153,15 @@ namespace cdt::utilities std::uint64_t digest{}; }; - [[nodiscard]] inline auto artifact_name(Artifact_kind const artifact) + [[nodiscard]] inline auto artifact_name(ArtifactKind const artifact) -> std::string_view { switch (artifact) { - case Artifact_kind::INITIAL_TRIANGULATION: + case ArtifactKind::INITIAL_TRIANGULATION: return "initial-triangulation"; - case Artifact_kind::CHECKPOINT: return "checkpoint"; - case Artifact_kind::FINAL_TRIANGULATION: return "final-triangulation"; + case ArtifactKind::CHECKPOINT: return "checkpoint"; + case ArtifactKind::FINAL_TRIANGULATION: return "final-triangulation"; } return "unknown"; } @@ -232,7 +233,7 @@ namespace cdt::utilities } template - inline bool constexpr HAS_CAUSAL_INFO = + inline constexpr bool HAS_CAUSAL_INFO = requires(TriangulationType const& triangulation) { triangulation.finite_vertex_handles(); triangulation.finite_cell_handles(); @@ -375,8 +376,7 @@ namespace cdt::utilities cdt::BUILD_SYSTEM_PROCESSOR, standard_library_name(), CGAL_VERSION_STR, metadata.seed, metadata.initialization_stream, metadata.transition_stream, - metadata.topology == topology_type::SPHERICAL ? "spherical" - : "toroidal", + metadata.topology == Topology::SPHERICAL ? "spherical" : "toroidal", metadata.dimension, metadata.desired_simplices, metadata.desired_timeslices, metadata.actual_vertices, metadata.actual_edges, metadata.actual_faces, @@ -394,6 +394,7 @@ namespace cdt::utilities append_optional("configured_passes", metadata.configured_passes); append_optional("checkpoint_interval", metadata.checkpoint_interval); append_optional("completed_passes", metadata.completed_passes); + append_optional("parallel.max_threads", metadata.max_threads); if (metadata.transition_trace) { text += fmt::format("transition_trace.fnv1a64={:016x}\n", @@ -636,21 +637,21 @@ namespace cdt::utilities struct Parsed_persistence_metadata { - Payload_integrity payload; - Artifact_kind artifact; - cdt::Random_seed seed; - cdt::Random_stream initialization_stream; - cdt::Random_stream transition_stream; - topology_type topology; - Int_precision dimension; - Int_precision actual_vertices; - Int_precision actual_edges; - Int_precision actual_faces; - Int_precision actual_simplices; - Int_precision minimum_timeslice; - Int_precision maximum_timeslice; - std::uint64_t placement_fingerprint; - std::uint64_t topology_fingerprint; + Payload_integrity payload; + ArtifactKind artifact; + cdt::RandomSeed seed; + cdt::RandomStream initialization_stream; + cdt::RandomStream transition_stream; + Topology topology; + Int_precision dimension; + Int_precision actual_vertices; + Int_precision actual_edges; + Int_precision actual_faces; + Int_precision actual_simplices; + Int_precision minimum_timeslice; + Int_precision maximum_timeslice; + std::uint64_t placement_fingerprint; + std::uint64_t topology_fingerprint; }; [[nodiscard]] inline auto read_persistence_metadata( @@ -761,18 +762,18 @@ namespace cdt::utilities std::make_error_code(std::errc::not_supported)); } - Artifact_kind artifact{}; + ArtifactKind artifact{}; if (values.at("artifact") == "initial-triangulation") { - artifact = Artifact_kind::INITIAL_TRIANGULATION; + artifact = ArtifactKind::INITIAL_TRIANGULATION; } else if (values.at("artifact") == "checkpoint") { - artifact = Artifact_kind::CHECKPOINT; + artifact = ArtifactKind::CHECKPOINT; } else if (values.at("artifact") == "final-triangulation") { - artifact = Artifact_kind::FINAL_TRIANGULATION; + artifact = ArtifactKind::FINAL_TRIANGULATION; } else { @@ -781,14 +782,14 @@ namespace cdt::utilities std::make_error_code(std::errc::illegal_byte_sequence)); } - topology_type topology{}; + Topology topology{}; if (values.at("topology") == "spherical") { - topology = topology_type::SPHERICAL; + topology = Topology::SPHERICAL; } else if (values.at("topology") == "toroidal") { - topology = topology_type::TOROIDAL; + topology = Topology::TOROIDAL; } else { @@ -875,7 +876,7 @@ namespace cdt::utilities std::make_error_code(std::errc::illegal_byte_sequence)); } - if (artifact == Artifact_kind::CHECKPOINT && + if (artifact == ArtifactKind::CHECKPOINT && !values.contains("completed_passes")) { throw std::filesystem::filesystem_error( @@ -889,6 +890,13 @@ namespace cdt::utilities "Persistence metadata contains invalid completed passes", path, std::make_error_code(std::errc::illegal_byte_sequence)); } + if (values.contains("parallel.max_threads") && + parse_unsigned(values.at("parallel.max_threads"), 10, path) == 0) + { + throw std::filesystem::filesystem_error( + "Persistence metadata contains an invalid thread limit", path, + std::make_error_code(std::errc::illegal_byte_sequence)); + } auto const transition_field_count = static_cast(values.contains("transition_trace.fnv1a64")) + @@ -911,19 +919,20 @@ namespace cdt::utilities .payload = {parse_unsigned(values.at("payload.size"), 10, path), parse_unsigned(values.at("payload.fnv1a64"), 16, path)}, .artifact = artifact, - .seed = parse_unsigned(values.at("random.seed"), 10, path), - .initialization_stream = parse_unsigned( - values.at("random.initialization_stream"), 10, path), - .transition_stream = - parse_unsigned(values.at("random.transition_stream"), 10, path), - .topology = topology, - .dimension = dimension, - .actual_vertices = actual_vertices, - .actual_edges = actual_edges, - .actual_faces = actual_faces, - .actual_simplices = actual_simplices, - .minimum_timeslice = minimum_timeslice, - .maximum_timeslice = maximum_timeslice, + .seed = cdt::RandomSeed{parse_unsigned(values.at("random.seed"), 10, + path)}, + .initialization_stream = cdt::RandomStream{parse_unsigned( + values.at("random.initialization_stream"), 10, path)}, + .transition_stream = cdt::RandomStream{parse_unsigned( + values.at("random.transition_stream"), 10, path)}, + .topology = topology, + .dimension = dimension, + .actual_vertices = actual_vertices, + .actual_edges = actual_edges, + .actual_faces = actual_faces, + .actual_simplices = actual_simplices, + .minimum_timeslice = minimum_timeslice, + .maximum_timeslice = maximum_timeslice, .placement_fingerprint = parse_unsigned(values.at("placement.fnv1a64"), 16, path), .topology_fingerprint = @@ -1355,7 +1364,7 @@ namespace cdt::utilities } // current_date_time /// @brief Generate useful filenames - /// @param t_topology The topology type from the scoped enum topology_type + /// @param t_topology The topology type from the scoped enum Topology /// @param t_dimension The dimensionality of the triangulation /// @param t_number_of_simplices The number of simplices in the /// triangulation @@ -1363,16 +1372,16 @@ namespace cdt::utilities /// @param t_initial_radius The radius of the first foliation t=1 /// @param t_foliation_spacing The spacing between foliations /// @returns A filename - [[nodiscard]] inline auto make_filename(topology_type const& t_topology, - Int_precision t_dimension, - Int_precision t_number_of_simplices, + [[nodiscard]] inline auto make_filename(Topology const& t_topology, + Int_precision t_dimension, + Int_precision t_number_of_simplices, Int_precision t_number_of_timeslices, double t_initial_radius, double t_foliation_spacing) -> std::filesystem::path { std::string filename; - if (t_topology == topology_type::SPHERICAL) { filename += "S"; } + if (t_topology == Topology::SPHERICAL) { filename += "S"; } else { filename += "T"; @@ -1418,20 +1427,20 @@ namespace cdt::utilities /// @brief Generate a filename that records the effective run seed. template - [[nodiscard]] auto make_filename(ManifoldType const& manifold, - cdt::Random_seed const seed) + [[nodiscard]] auto make_filename(ManifoldType const& manifold, + cdt::RandomSeed const seed) { auto const base = make_filename(manifold); return base.parent_path() / - (base.stem().string() + "-seed-" + std::to_string(seed) + + (base.stem().string() + "-seed-" + std::to_string(seed.value()) + 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) + [[nodiscard]] auto make_filename(ManifoldType const& manifold, + cdt::RandomSeed const seed, + Int_precision const completed_passes) { auto const base = make_filename(manifold, seed); return base.parent_path() / @@ -1499,8 +1508,8 @@ namespace cdt::utilities /// @brief Build provenance from a canonical manifold state. template [[nodiscard]] auto make_reproducibility_metadata(ManifoldType const& manifold, - cdt::Random_seed const seed, - Artifact_kind const artifact) + cdt::RandomSeed const seed, + ArtifactKind const artifact) -> Reproducibility_metadata { return {.artifact = artifact, @@ -1556,21 +1565,21 @@ namespace cdt::utilities /// @brief Write a triangulation with the effective seed in its filename. template - void write_file(ManifoldType const& t_universe, cdt::Random_seed const seed) + void write_file(ManifoldType const& t_universe, cdt::RandomSeed const seed) { auto const metadata = make_reproducibility_metadata( - t_universe, seed, Artifact_kind::FINAL_TRIANGULATION); + t_universe, seed, ArtifactKind::FINAL_TRIANGULATION); write_file(make_filename(t_universe, seed), t_universe.delaunay_snapshot(), metadata); } /// @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, + void write_file(ManifoldType const& t_universe, cdt::RandomSeed const seed, Int_precision const completed_passes) { auto metadata = make_reproducibility_metadata(t_universe, seed, - Artifact_kind::CHECKPOINT); + ArtifactKind::CHECKPOINT); metadata.completed_passes = completed_passes; write_file(make_filename(t_universe, seed, completed_passes), t_universe.delaunay_snapshot(), metadata); @@ -1582,7 +1591,7 @@ namespace cdt::utilities Reproducibility_metadata const& metadata) { auto filename = make_filename(universe, metadata.seed); - if (metadata.artifact == Artifact_kind::CHECKPOINT) + if (metadata.artifact == ArtifactKind::CHECKPOINT) { if (!metadata.completed_passes) { @@ -1600,7 +1609,8 @@ namespace cdt::utilities /// @param filename The file to read from /// @returns A Delaunay triangulation template - auto read_file(std::filesystem::path const& filename) -> TriangulationType + [[nodiscard]] auto read_file(std::filesystem::path const& filename) + -> TriangulationType { static std::mutex mutex; fmt::print("Reading from file {}\n", filename.string()); @@ -1654,7 +1664,8 @@ namespace cdt::utilities /// @brief Generate random integers by calling generate_random, preserves /// template argument deduction - template + template [[nodiscard]] auto generate_random_int(Generator& generator, IntegerType t_min_value, IntegerType t_max_value) @@ -1665,7 +1676,8 @@ namespace cdt::utilities } // generate_random_int() /// @brief Generate a random timeslice - template + template [[nodiscard]] auto generate_random_timeslice(Generator& generator, IntegerType t_max_timeslice) -> decltype(auto) @@ -1677,7 +1689,7 @@ namespace cdt::utilities /// @brief Generate random real numbers by calling generate_random, /// preserves template argument deduction template + std::floating_point FloatingPointType> [[nodiscard]] auto generate_random_real(Generator& generator, FloatingPointType t_min_value, FloatingPointType t_max_value) @@ -1691,8 +1703,8 @@ namespace cdt::utilities template [[nodiscard]] inline auto generate_probability(Generator& generator) { - auto constexpr min = 0.0L; - auto constexpr max = 1.0L; + constexpr auto min = 0.0L; + constexpr auto max = 1.0L; return generate_random_real(generator, min, max); } // generate_probability() @@ -1709,7 +1721,7 @@ namespace cdt::utilities /// @param t_number_of_timeslices Number of desired timeslices /// @returns The number of points per timeslice to obtain /// the desired number of simplices - inline auto expected_points_per_timeslice( + [[nodiscard]] inline auto expected_points_per_timeslice( Int_precision const t_dimension, Int_precision t_number_of_simplices, Int_precision t_number_of_timeslices) { @@ -1795,8 +1807,8 @@ namespace cdt::utilities /// /// @param t_value An exact Gmpzf multiple-precision floating point number /// @returns The double conversion - [[nodiscard]] inline auto Gmpzf_to_double(Gmpzf const& t_value) -> double - { return t_value.to_double(); } // Gmpzf_to_double + [[nodiscard]] inline auto gmpzf_to_double(Gmpzf const& t_value) -> double + { return t_value.to_double(); } // gmpzf_to_double /// @brief Create console and file loggers /// @details Create a console and file loggers. @@ -1868,7 +1880,7 @@ namespace cdt::utilities /// @param t_point The point /// @returns A string representation of the point template - auto point_to_str(Point const& t_point) -> std::string + [[nodiscard]] auto point_to_str(Point const& t_point) -> std::string { std::stringstream stream; stream << t_point; @@ -1876,9 +1888,10 @@ namespace cdt::utilities } // point_to_str /// @brief Convert a topology to a string using it's << operator - /// @param t_topology The topology_type to convert - /// @returns A string representation of the topology_type - inline auto topology_to_str(topology_type const& t_topology) -> std::string + /// @param t_topology The Topology to convert + /// @returns A string representation of the Topology + [[nodiscard]] inline auto topology_to_str(Topology const& t_topology) + -> std::string { std::stringstream stream; stream << t_topology; diff --git a/scripts/build.bat b/scripts/build.bat index 7a80c4b87..f8ff5608a 100644 --- a/scripts/build.bat +++ b/scripts/build.bat @@ -3,6 +3,17 @@ SETLOCAL ENABLEEXTENSIONS SET "SCRIPT_DIR=%~dp0" FOR %%I IN ("%SCRIPT_DIR%..") DO SET "REPO_ROOT=%%~fI" +IF NOT "%~2"=="" GOTO PRESET_USAGE +SET "PRESET=%~1" +IF NOT DEFINED PRESET SET "PRESET=reference" +IF /I "%PRESET%"=="reference" GOTO PRESET_READY +IF /I "%PRESET%"=="parallel" GOTO PRESET_READY + +:PRESET_USAGE +echo Usage: %~nx0 [reference^|parallel] 1>&2 +EXIT /B 2 + +:PRESET_READY IF DEFINED CDT_VCPKG_CACHE_DIR ( SET "PINNED_VCPKG_ROOT=%CDT_VCPKG_CACHE_DIR%" @@ -25,15 +36,15 @@ python.exe "%SCRIPT_DIR%bootstrap_vcpkg.py" || EXIT /B 1 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 --parallel 2 || EXIT /B 1 -ctest --preset reference-smoke || EXIT /B 1 +cmake --preset "%PRESET%" -S . || EXIT /B 1 +cmake --build --preset "%PRESET%" --parallel 2 || EXIT /B 1 +ctest --preset "%PRESET%-smoke" || EXIT /B 1 EXIT /B 0 :PREPARE_CMAKE_CACHE -SET "CMAKE_CACHE=%REPO_ROOT%\out\build\reference\CMakeCache.txt" +SET "CMAKE_CACHE=%REPO_ROOT%\out\build\%PRESET%\CMakeCache.txt" SET "EXPECTED_TOOLCHAIN=%VCPKG_ROOT:\=/%/scripts/buildsystems/vcpkg.cmake" -SET "EXPECTED_INSTALLED=%REPO_ROOT:\=/%/out/build/reference/vcpkg_installed" +SET "EXPECTED_INSTALLED=%REPO_ROOT:\=/%/out/build/%PRESET%/vcpkg_installed" SET "CACHED_TOOLCHAIN=" SET "CACHED_INSTALLED=" IF EXIST "%CMAKE_CACHE%" FOR /F "tokens=1,* delims==" %%A IN ('FINDSTR /B /C:"CMAKE_TOOLCHAIN_FILE:" "%CMAKE_CACHE%"') DO SET "CACHED_TOOLCHAIN=%%B" @@ -47,5 +58,5 @@ EXIT /B 0 :REFRESH_CMAKE_CACHE ECHO vcpkg configuration changed; refreshing CMake configuration state. 1>&2 DEL /F /Q "%CMAKE_CACHE%" || EXIT /B 1 -IF EXIST "%REPO_ROOT%\out\build\reference\CMakeFiles\." RMDIR /S /Q "%REPO_ROOT%\out\build\reference\CMakeFiles" || EXIT /B 1 +IF EXIST "%REPO_ROOT%\out\build\%PRESET%\CMakeFiles\." RMDIR /S /Q "%REPO_ROOT%\out\build\%PRESET%\CMakeFiles" || EXIT /B 1 EXIT /B 0 diff --git a/scripts/build.sh b/scripts/build.sh index 88559f01d..7a0550369 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -4,14 +4,27 @@ set -euo pipefail script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd -- "${script_dir}/.." && pwd)" -reference_build_dir="${repo_root}/out/build/reference" +if [[ "$#" -gt 1 ]]; then + printf 'Usage: %s [reference|parallel]\n' "$0" >&2 + exit 2 +fi +preset="${1:-reference}" +case "${preset}" in + reference | parallel) ;; + *) + printf 'Unsupported build preset %s; expected reference or parallel.\n' \ + "${preset}" >&2 + exit 2 + ;; +esac +build_dir="${repo_root}/out/build/${preset}" source "${script_dir}/prepare-vcpkg.sh" prepare_reference_environment prepare_vcpkg "${repo_root}" -prepare_cmake_cache "${reference_build_dir}" +prepare_cmake_cache "${build_dir}" cd -- "${repo_root}" -cmake --preset reference -S "${repo_root}" -cmake --build --preset reference -ctest --preset reference-smoke +cmake --preset "${preset}" -S "${repo_root}" +cmake --build --preset "${preset}" +ctest --preset "${preset}-smoke" diff --git a/scripts/pkgx-build.sh b/scripts/pkgx-build.sh index 319b7ecc6..b12de4de4 100755 --- a/scripts/pkgx-build.sh +++ b/scripts/pkgx-build.sh @@ -33,9 +33,20 @@ if [[ "${1:-}" == "--codeql" ]]; then exec pkgx "${pkgx_tools[@]}" -- "${script_dir}/codeql-build.sh" "$@" fi +if [[ "${1:-}" == "--preset" ]]; then + if [[ "$#" -ne 2 ]] || [[ "$2" != "reference" && "$2" != "parallel" ]]; then + printf 'Usage: %s [--preset reference|parallel] [--codeql prepare|build]\n' \ + "$0" >&2 + exit 2 + fi + preset="$2" + exec pkgx "${pkgx_tools[@]}" -- "${script_dir}/build.sh" "${preset}" +fi + if [[ "$#" -ne 0 ]]; then - printf 'Usage: %s [--codeql prepare|build]\n' "$0" >&2 + printf 'Usage: %s [--preset reference|parallel] [--codeql prepare|build]\n' \ + "$0" >&2 exit 2 fi -exec pkgx "${pkgx_tools[@]}" -- "${script_dir}/build.sh" +exec pkgx "${pkgx_tools[@]}" -- "${script_dir}/build.sh" reference diff --git a/scripts/slurm.sh b/scripts/slurm.sh index a1fc7483c..3987cc9a6 100755 --- a/scripts/slurm.sh +++ b/scripts/slurm.sh @@ -9,7 +9,7 @@ script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd -- "${script_dir}/.." && pwd)" build_dir="${repo_root}/build" -module load cmake/3.28.1 +module load cmake/4.4.0 module load gcc/13.3.0 module load autoconf-archive/2022.02.11 rm -rf -- "${build_dir}" diff --git a/semgrep.yaml b/semgrep.yaml index f544c3d44..773f9e571 100644 --- a/semgrep.yaml +++ b/semgrep.yaml @@ -75,7 +75,7 @@ rules: regex: ^m_(?:attempted|successful|succeeded|failed|proposed|accepted|rejected)_moves$ - metavariable-regex: metavariable: $GETTER - regex: ^get_(?:attempted|successful|succeeded|failed|proposed|accepted|rejected)$ + regex: ^(?:attempted|successful|succeeded|failed|proposed|accepted|rejected)$ - id: cdt.cpp.move-run-cadence-is-parsed-once languages: - cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e2ba2f405..b99b1ac51 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -36,6 +36,11 @@ function(add_cli_failure_test test_name target expected_regex) set_tests_properties(${test_name} PROPERTIES LABELS "integration;cli-boundary") endfunction() +set(cdt_cli_explicit_threads 1) +if(ENABLE_PARALLEL_TRIANGULATION) + set(cdt_cli_explicit_threads 2) +endif() + add_test( NAME cdt COMMAND @@ -44,6 +49,7 @@ add_test( "-DTEST_DIRECTORY=${CMAKE_CURRENT_BINARY_DIR}/cdt-output" "-DEXPECTED_ARTIFACT=final-triangulation" "-DEXPECTED_SEED=92" + "-DEXPECTED_THREADS=1" "-DTEST_ARGUMENTS=-s;-n127;-t4;-a0.6;-k1.1;-l0.1;-p1;--seed;92" -P ${PROJECT_SOURCE_DIR}/cmake/RunPersistenceOutputTest.cmake) set_tests_properties(cdt PROPERTIES LABELS "integration;persistence") @@ -54,6 +60,7 @@ add_test( ${CMAKE_COMMAND} -DCDT_EXECUTABLE=$ -DTEST_ROOT=${CMAKE_CURRENT_BINARY_DIR} -DTEST_DIRECTORY=${CMAKE_CURRENT_BINARY_DIR}/cdt-no-output + -DTEST_THREADS=${cdt_cli_explicit_threads} -P ${PROJECT_SOURCE_DIR}/cmake/RunCdtNoOutputTest.cmake) set_tests_properties(cdt-no-output PROPERTIES LABELS "integration") @@ -76,6 +83,8 @@ add_cli_failure_test(cdt-spacing-zero cdt "Foliation spacing must be positive." 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 --seed 92) +add_cli_failure_test(cdt-threads-zero cdt "Thread count must be positive." -s -n64 -t3 -a0.6 -k1.1 -l0.1 + --threads 0 --seed 92) add_test( NAME initialize @@ -85,7 +94,8 @@ add_test( "-DTEST_DIRECTORY=${CMAKE_CURRENT_BINARY_DIR}/initialize-output" "-DEXPECTED_ARTIFACT=initial-triangulation" "-DEXPECTED_SEED=92" - "-DTEST_ARGUMENTS=-s;-n640;-t4;-o;--seed;92" + "-DEXPECTED_THREADS=${cdt_cli_explicit_threads}" + "-DTEST_ARGUMENTS=-s;-n640;-t4;-o;--seed;92;--threads;${cdt_cli_explicit_threads}" -P ${PROJECT_SOURCE_DIR}/cmake/RunPersistenceOutputTest.cmake) set_tests_properties(initialize PROPERTIES LABELS "integration;persistence") @@ -96,6 +106,7 @@ add_test( "-DTEST_ROOT=${CMAKE_CURRENT_BINARY_DIR}" "-DTEST_DIRECTORY=${CMAKE_CURRENT_BINARY_DIR}/initialize-generated-seed-output" "-DEXPECTED_ARTIFACT=initial-triangulation" + "-DEXPECTED_THREADS=1" "-DTEST_ARGUMENTS=-s;-n64;-t3;-o" -P ${PROJECT_SOURCE_DIR}/cmake/RunPersistenceOutputTest.cmake) set_tests_properties(initialize-generated-seed PROPERTIES LABELS "integration;persistence") @@ -106,3 +117,5 @@ add_cli_failure_test(initialize-dimensionality initialize "Only three-dimensiona 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) +add_cli_failure_test(initialize-threads-zero initialize "Thread count must be positive." -s -n64 -t3 --threads 0 + --seed 92) diff --git a/src/cdt-viewer.cpp b/src/cdt-viewer.cpp index db08cfdbf..ca9345fad 100644 --- a/src/cdt-viewer.cpp +++ b/src/cdt-viewer.cpp @@ -19,7 +19,7 @@ Copyright © 2022 Adam Getchell using namespace cdt; namespace po = boost::program_options; -static auto constexpr USAGE = +static constexpr auto USAGE = R"(Causal Dynamical Triangulations in C++ using CGAL. Copyright (c) 2022 Adam Getchell diff --git a/src/cdt.cpp b/src/cdt.cpp index 5f69f7f21..393e4fbfd 100644 --- a/src/cdt.cpp +++ b/src/cdt.cpp @@ -13,6 +13,11 @@ #include #include +#if defined(CDT_ENABLE_PARALLEL_TRIANGULATION) && \ + CDT_ENABLE_PARALLEL_TRIANGULATION +#include +#endif + #include #include #include @@ -27,7 +32,7 @@ using namespace std; namespace po = boost::program_options; /// Help text used by Boost.Program_options -static string_view constexpr USAGE{ +static constexpr string_view USAGE{ R"(Causal Dynamical Triangulations in C++ using CGAL. Copyright (c) 2013-2026 Adam Getchell @@ -44,6 +49,7 @@ Usage:./cdt (--spherical | --toroidal) -n SIMPLICES -t TIMESLICES [--foliate FOLIATION SPACING] [--no-output] [--seed SEED] + [--threads THREADS] -k K --alpha ALPHA --lambda LAMBDA @@ -78,6 +84,7 @@ try long long passes{}; long long checkpoint{}; std::uint64_t seed{}; + long long threads{}; po::options_description description(intro); description.add_options()("help,h", "Show this message")( @@ -96,6 +103,8 @@ try "no-output", "Do not write checkpoint or final triangulation files")( "seed", po::value(&seed), "Root random seed (default: operating-system entropy)")( + "threads", po::value(&threads)->default_value(1), + "Maximum worker threads for supported Delaunay operations")( "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)")( @@ -136,10 +145,16 @@ try auto const triangulation_config = runtime_config::make_triangulation( args.count("spherical") != 0, args.count("toroidal") != 0, simplices, timeslices, dimensions, initial_radius, foliation_spacing, - root_random.seed()); + root_random.seed(), threads); auto const config = runtime_config::make_simulation( triangulation_config, alpha, k, lambda, passes, checkpoint, !args.count("no-output")); +#if defined(CDT_ENABLE_PARALLEL_TRIANGULATION) && \ + CDT_ENABLE_PARALLEL_TRIANGULATION + [[maybe_unused]] oneapi::tbb::global_control thread_limit{ + oneapi::tbb::global_control::max_allowed_parallelism, + config.triangulation().threads()}; +#endif auto initialization_random = root_random.split(cdt::random_streams::initialization); auto transition_random = root_random.split(cdt::random_streams::transitions); @@ -159,6 +174,8 @@ try fmt::print("Number of passes: {}\n", config.passes()); fmt::print("Checkpoint every {} passes.\n", config.checkpoint()); fmt::print("Effective random seed: {}\n", config.triangulation().seed()); + fmt::print("Maximum Delaunay threads: {}\n", + config.triangulation().threads()); fmt::print("=== Parameters ===\n"); fmt::print("Alpha: {}\n", config.alpha()); fmt::print("K: {}\n", config.k()); @@ -180,7 +197,7 @@ try auto reproducibility = utilities::make_reproducibility_metadata( universe, config.triangulation().seed(), - utilities::Artifact_kind::FINAL_TRIANGULATION); + utilities::ArtifactKind::FINAL_TRIANGULATION); reproducibility.desired_simplices = config.triangulation().simplices(); reproducibility.desired_timeslices = config.triangulation().timeslices(); reproducibility.alpha = config.alpha(); @@ -188,6 +205,7 @@ try reproducibility.lambda = config.lambda(); reproducibility.configured_passes = config.passes(); reproducibility.checkpoint_interval = config.checkpoint(); + reproducibility.max_threads = config.triangulation().threads(); // Initialize the Metropolis algorithm with complete run provenance. Metropolis_3 run(config.alpha(), config.k(), config.lambda(), config.passes(), @@ -225,7 +243,7 @@ try { utilities::write_file( result, run.reproducibility_metadata( - result, utilities::Artifact_kind::FINAL_TRIANGULATION, + result, utilities::ArtifactKind::FINAL_TRIANGULATION, config.passes())); } diff --git a/src/initialize.cpp b/src/initialize.cpp index 7c30c9e44..08e7c6f86 100644 --- a/src/initialize.cpp +++ b/src/initialize.cpp @@ -9,6 +9,11 @@ /// @author Adam Getchell #include +#if defined(CDT_ENABLE_PARALLEL_TRIANGULATION) && \ + CDT_ENABLE_PARALLEL_TRIANGULATION +#include +#endif + #include #include "Manifold.hpp" @@ -19,7 +24,7 @@ using namespace cdt; using namespace std; namespace po = boost::program_options; -static string_view constexpr USAGE{ +static constexpr string_view USAGE{ R"(Causal Dynamical Triangulations in C++ using CGAL. Copyright (c) 2014 Adam Getchell @@ -34,6 +39,7 @@ Usage:./initialize (--spherical | --toroidal) -n SIMPLICES -t TIMESLICES [--init INITIAL RADIUS] [--foliate FOLIATION SPACING] [--seed SEED] + [--threads THREADS] [--output] Optional arguments are in square brackets. @@ -55,6 +61,7 @@ try double initial_radius{}; double foliation_spacing{}; std::uint64_t seed{}; + long long threads{}; po::options_description description(intro); description.add_options()("help,h", "Show this message")( @@ -72,6 +79,8 @@ try "Foliation spacing")( "seed", po::value(&seed), "Root random seed (default: operating-system entropy)")( + "threads", po::value(&threads)->default_value(1), + "Maximum worker threads for supported Delaunay operations")( "output,o", "Save triangulation into OFF file"); po::variables_map args; @@ -105,7 +114,12 @@ try auto const config = runtime_config::make_triangulation( args.count("spherical") != 0, args.count("toroidal") != 0, simplices, timeslices, dimensions, initial_radius, foliation_spacing, - root_random.seed()); + root_random.seed(), threads); +#if defined(CDT_ENABLE_PARALLEL_TRIANGULATION) && \ + CDT_ENABLE_PARALLEL_TRIANGULATION + [[maybe_unused]] oneapi::tbb::global_control thread_limit{ + oneapi::tbb::global_control::max_allowed_parallelism, config.threads()}; +#endif auto const save_file = args.count("output") != 0; auto initialization_random = root_random.split(cdt::random_streams::initialization); @@ -118,6 +132,7 @@ try 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()); + fmt::print("Maximum Delaunay threads: {}\n", config.threads()); if (save_file) { fmt::print("Output will be saved.\n"); } @@ -131,9 +146,10 @@ try { auto metadata = utilities::make_reproducibility_metadata( universe, config.seed(), - utilities::Artifact_kind::INITIAL_TRIANGULATION); + utilities::ArtifactKind::INITIAL_TRIANGULATION); metadata.desired_simplices = config.simplices(); metadata.desired_timeslices = config.timeslices(); + metadata.max_threads = config.threads(); utilities::write_file(universe, metadata); } return EXIT_SUCCESS; diff --git a/tests/Apply_move_test.cpp b/tests/Apply_move_test.cpp index e9d450f7d..63f5996fe 100644 --- a/tests/Apply_move_test.cpp +++ b/tests/Apply_move_test.cpp @@ -25,12 +25,12 @@ using namespace manifolds; namespace { - static inline auto constexpr RADIUS_2 = + static inline constexpr auto 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; + static inline constexpr auto SQRT_2 = std::numbers::sqrt2_v; + static inline constexpr auto INV_SQRT_2 = 1.0 / SQRT_2; - [[nodiscard]] auto make_23_move_manifold() -> Manifold_3 + [[nodiscard]] auto make_23_move_manifold() -> Manifold_3 { vector vertices{ Point_t<3>{ 1, 0, 0}, @@ -71,7 +71,7 @@ namespace } void check_applied_move(Manifold_3 const& before, Manifold_3 const& after, - move_tracker::move_type const move) + move_tracker::MoveType const move) { CHECK(before.is_correct()); CHECK(after.is_correct()); @@ -111,7 +111,7 @@ SCENARIO("apply_move forwards deterministic ergodic moves" * 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); } + { check_applied_move(before, after, move_tracker::MoveType::TWO_THREE); } } } @@ -129,7 +129,7 @@ SCENARIO("apply_move forwards deterministic ergodic moves" * 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); } + { check_applied_move(before, after, move_tracker::MoveType::THREE_TWO); } } } @@ -145,7 +145,7 @@ SCENARIO("apply_move forwards deterministic ergodic moves" * 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); } + { check_applied_move(before, after, move_tracker::MoveType::TWO_SIX); } } } @@ -163,7 +163,7 @@ SCENARIO("apply_move forwards deterministic ergodic moves" * 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); } + { check_applied_move(before, after, move_tracker::MoveType::SIX_TWO); } } } @@ -179,7 +179,7 @@ SCENARIO("apply_move forwards deterministic ergodic moves" * 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); } + { check_applied_move(before, after, move_tracker::MoveType::FOUR_FOUR); } } } } diff --git a/tests/Bistellar_flip_test.cpp b/tests/Bistellar_flip_test.cpp index f69695802..ea05e521e 100644 --- a/tests/Bistellar_flip_test.cpp +++ b/tests/Bistellar_flip_test.cpp @@ -28,9 +28,9 @@ using namespace ergodic_moves::detail; namespace { // Constants for testing - static inline std::floating_point auto constexpr SQRT_2 = + static inline constexpr std::floating_point auto SQRT_2 = std::numbers::sqrt2_v; - static inline std::floating_point auto constexpr INV_SQRT_2 = 1 / SQRT_2; + static inline constexpr std::floating_point auto INV_SQRT_2 = 1 / SQRT_2; // Helper function to create a triangulation for testing auto create_test_triangulation() -> Delaunay @@ -213,35 +213,19 @@ SCENARIO("Verify cell orientation and vertex ordering after bistellar flip" * THEN("All cells have correct orientation") { - // CGAL's is_valid() checks for proper orientation - // FoliatedTriangulation doesn't have is_valid() method - // Validity will be checked through the Manifold_3 below + CHECK(flipped_triangulation->is_valid()); + CHECK(flipped_triangulation->tds().is_valid()); - AND_THEN("Vertex ordering is consistent") + AND_THEN("Every finite cell has positive orientation") { - bool consistent_ordering = true; for (auto cit = flipped_triangulation->finite_cells_begin(); cit != flipped_triangulation->finite_cells_end(); ++cit) { - // CGAL ensures that vertices are ordered so that the orientation is - // positive This is implicitly checked by is_valid(), but we can - // verify the determinant is positive - auto v0 = cit->vertex(0)->point(); - auto v1 = cit->vertex(1)->point(); - auto v2 = cit->vertex(2)->point(); - auto v3 = cit->vertex(3)->point(); - - // Orientation is checked by CGAL's is_valid, so we don't need to - // manually calculate it Just check that each vertex is unique - if (v0 == v1 || v0 == v2 || v0 == v3 || v1 == v2 || v1 == v3 || - v2 == v3) - { - consistent_ordering = false; - break; - } + CHECK_EQ(CGAL::orientation( + cit->vertex(0)->point(), cit->vertex(1)->point(), + cit->vertex(2)->point(), cit->vertex(3)->point()), + CGAL::POSITIVE); } - - CHECK(consistent_ordering); } } } @@ -269,12 +253,14 @@ SCENARIO("Test edge cases and error conditions for bistellar flip" * invalid_edge.second = 0; invalid_edge.third = 1; + auto const original = triangulation; auto result = bistellar_flip(triangulation, invalid_edge, top, bottom); - THEN("The bistellar flip should fail") + THEN("The bistellar flip should fail without changing the source") { CHECK_FALSE(result.has_value()); CHECK(triangulation.is_valid()); + CHECK_EQ(triangulation, original); } } @@ -302,33 +288,47 @@ SCENARIO("Test edge cases and error conditions for bistellar flip" * WHEN("We provide null vertex handles") { - auto result = + auto const original = triangulation; + auto result = bistellar_flip(triangulation, pivot_edge.value(), nullptr, bottom); - THEN("The bistellar flip should fail") - { CHECK_FALSE(result.has_value()); } + THEN("The top-handle failure should not change the source") + { + CHECK_FALSE(result.has_value()); + CHECK_EQ(triangulation, original); + } auto result2 = bistellar_flip(triangulation, pivot_edge.value(), top, nullptr); - THEN("The bistellar flip should fail") - { CHECK_FALSE(result2.has_value()); } + THEN("The bottom-handle failure should not change the source") + { + CHECK_FALSE(result2.has_value()); + CHECK_EQ(triangulation, original); + } } WHEN("We provide infinite vertices") { - auto infinite_vertex = triangulation.infinite_vertex(); - auto result = bistellar_flip(triangulation, pivot_edge.value(), - infinite_vertex, bottom); + auto const original = triangulation; + auto infinite_vertex = triangulation.infinite_vertex(); + auto result = bistellar_flip(triangulation, pivot_edge.value(), + infinite_vertex, bottom); - THEN("The bistellar flip should fail") - { CHECK_FALSE(result.has_value()); } + THEN("The top-vertex failure should not change the source") + { + CHECK_FALSE(result.has_value()); + CHECK_EQ(triangulation, original); + } auto result2 = bistellar_flip(triangulation, pivot_edge.value(), top, infinite_vertex); - THEN("The bistellar flip should fail") - { CHECK_FALSE(result2.has_value()); } + THEN("The bottom-vertex failure should not change the source") + { + CHECK_FALSE(result2.has_value()); + CHECK_EQ(triangulation, original); + } } } } diff --git a/tests/CGAL_benchmark.cpp b/tests/CGAL_benchmark.cpp index 64ff21573..6124b5499 100644 --- a/tests/CGAL_benchmark.cpp +++ b/tests/CGAL_benchmark.cpp @@ -8,6 +8,11 @@ /// @brief Repeatable diagnostics for the production CGAL integration boundary #include +#if defined(CDT_ENABLE_PARALLEL_TRIANGULATION) && \ + CDT_ENABLE_PARALLEL_TRIANGULATION +#include +#include +#endif #include #include @@ -20,15 +25,19 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include "Manifold.hpp" #include "Move_command.hpp" +#include "Utilities.hpp" +#include "Version.hpp" namespace { @@ -55,7 +64,14 @@ namespace std::ranges::sort(ordered); std::cout << name << "_ns_min=" << ordered.front() << '\n' << name << "_ns_median=" << ordered[ordered.size() / 2] << '\n' - << name << "_ns_max=" << ordered.back() << '\n'; + << name << "_ns_max=" << ordered.back() << '\n' + << name << "_ns_samples="; + for (std::size_t index = 0; index < samples.size(); ++index) + { + if (index != 0) { std::cout << ','; } + std::cout << samples[index]; + } + std::cout << '\n'; } }; @@ -116,18 +132,42 @@ namespace auto main(int const argc, char const* const argv[]) -> int try { - if (argc > 4) + if (argc > 6) { throw std::invalid_argument{ - "usage: CDT_cgal_benchmark [simplices] [repetitions] [moves]"}; + "usage: CDT_cgal_benchmark [simplices] [repetitions] [moves] " + "[threads] [warmups]"}; } auto const simplices = argc > 1 ? parse_positive(argv[1], "simplices") : 640; auto const repetitions = argc > 2 ? parse_positive(argv[2], "repetitions") : 5; auto const move_count = argc > 3 ? parse_positive(argv[3], "moves") : 50; - auto constexpr timeslices = cdt::Int_precision{4}; + auto const thread_count = argc > 4 ? parse_positive(argv[4], "threads") : 1; + auto const warmups = argc > 5 ? parse_positive(argv[5], "warmups") : 1; + constexpr auto timeslices = cdt::Int_precision{4}; + constexpr auto seed = cdt::RandomSeed{102}; + if (warmups > std::numeric_limits::max() - repetitions) + { + throw std::out_of_range{"warmups plus repetitions is too large"}; + } + +#if defined(CDT_ENABLE_PARALLEL_TRIANGULATION) && \ + CDT_ENABLE_PARALLEL_TRIANGULATION + oneapi::tbb::global_control thread_limit{ + oneapi::tbb::global_control::max_allowed_parallelism, + gsl::narrow(thread_count)}; + auto const active_threads = oneapi::tbb::global_control::active_value( + oneapi::tbb::global_control::max_allowed_parallelism); +#else + if (thread_count != 1) + { + throw std::invalid_argument{ + "sequential triangulation benchmarks require threads=1"}; + } + constexpr auto active_threads = std::size_t{1}; +#endif - cdt::Random input_random{102}; + cdt::Random input_random{seed}; auto const input = cdt::foliated_triangulations::make_foliated_ball<3>( simplices, timeslices, cdt::INITIAL_RADIUS, cdt::FOLIATION_SPACING, input_random); @@ -143,23 +183,24 @@ try cdt::Int_precision final_vertices{}; cdt::Int_precision final_cells{}; - for (cdt::Int_precision repetition = 0; repetition < repetitions; - ++repetition) + auto const total_runs = warmups + repetitions; + for (cdt::Int_precision run = 0; run < total_runs; ++run) { + auto const record = run >= warmups; auto [insert_time, state] = measure([&input] { return cdt::detail::Delaunay_state<3>{input}; }); - bulk_insert.add(insert_time); + if (record) { bulk_insert.add(insert_time); } auto [repair_time, repair_passes] = measure( [&state] { return repair(state.mutable_triangulation_unchecked()); }); - foliation_repair.add(repair_time); + if (record) { foliation_repair.add(repair_time); } auto detached = std::move(state).into_detached_triangulation(); auto [cache_time, manifold] = measure([&detached] { return Manifold{cdt::foliated_triangulations::FoliatedTriangulation_3{ std::move(detached)}}; }); - cache_rebuild.add(cache_time); + if (record) { cache_rebuild.add(cache_time); } if (!manifold.is_correct()) { throw std::runtime_error{ @@ -168,7 +209,7 @@ try auto [copy_time, snapshot] = measure([&manifold] { return manifold.delaunay_snapshot(); }); - snapshot_copy.add(copy_time); + if (record) { snapshot_copy.add(copy_time); } std::vector> points; points.reserve(snapshot.number_of_vertices()); @@ -181,7 +222,7 @@ try .has_value(); }); }); - point_lookup.add(lookup_time); + if (record) { point_lookup.add(lookup_time); } std::vector> to_remove; auto const removal_count = @@ -195,54 +236,87 @@ try auto [remove_time, removed] = measure([&snapshot, &to_remove] { return snapshot.remove(to_remove.begin(), to_remove.end()); }); - vertex_removal.add(remove_time); + if (record) { vertex_removal.add(remove_time); } if (!snapshot.is_valid()) { throw std::runtime_error{"benchmark removal invalidated the CGAL TDS"}; } cdt::MoveCommand command{manifold}; - auto constexpr moves = std::array{ - cdt::move_tracker::move_type::TWO_THREE, - cdt::move_tracker::move_type::THREE_TWO, - cdt::move_tracker::move_type::TWO_SIX, - cdt::move_tracker::move_type::SIX_TWO, - cdt::move_tracker::move_type::FOUR_FOUR, + constexpr auto moves = std::array{ + cdt::move_tracker::MoveType::TWO_THREE, + cdt::move_tracker::MoveType::THREE_TWO, + cdt::move_tracker::MoveType::TWO_SIX, + cdt::move_tracker::MoveType::SIX_TWO, + cdt::move_tracker::MoveType::FOUR_FOUR, }; for (cdt::Int_precision index = 0; index < move_count; ++index) { command.enqueue(moves[static_cast(index) % moves.size()]); } - cdt::Random move_random{102}; + cdt::Random move_random{seed, cdt::random_streams::transitions}; auto [moves_time, final_topology] = measure([&command, &move_random] { command.execute(move_random); - auto const& result = command.get_const_results(); + auto const& result = command.result(); return Topology_counts{result.N0(), result.N3()}; }); - move_workload.add(moves_time); - if (!command.get_const_results().is_correct()) + if (record) { move_workload.add(moves_time); } + if (!command.result().is_correct()) { throw std::runtime_error{ "benchmark move workload violated CDT invariants"}; } - final_vertices = final_topology.vertices; - final_cells = final_topology.cells; - checksum += checksum_component(repair_passes) + checksum_component(found) + - checksum_component(removed) + - checksum_component(final_vertices) + - checksum_component(final_cells); + if (record) + { + final_vertices = final_topology.vertices; + final_cells = final_topology.cells; + checksum += checksum_component(repair_passes) + + checksum_component(found) + checksum_component(removed) + + checksum_component(final_vertices) + + checksum_component(final_cells); + } } - std::cout << "cgal_version=" << CGAL_VERSION_STR << '\n' + std::cout << "record.schema=cdt-cgal-benchmark-v1\n" + << "implementation.name=CDT-plusplus\n" + << "implementation.version=" << cdt::VERSION << '\n' + << "implementation.revision=" << cdt::SOURCE_REVISION << '\n' + << "build.compiler_id=" << cdt::BUILD_COMPILER_ID << '\n' + << "build.compiler_version=" << cdt::BUILD_COMPILER_VERSION << '\n' + << "build.configuration=" << cdt::BUILD_CONFIGURATION << '\n' + << "build.system=" << cdt::BUILD_SYSTEM_NAME << '\n' + << "build.processor=" << cdt::BUILD_SYSTEM_PROCESSOR << '\n' + << "build.cxx_standard=23\n" + << "build.standard_library=" + << cdt::utilities::detail::standard_library_name() << '\n' + << "hardware.logical_threads=" + << std::thread::hardware_concurrency() << '\n' + << "dependency.cgal_version=" << CGAL_VERSION_STR << '\n' +#if defined(CDT_ENABLE_PARALLEL_TRIANGULATION) && \ + CDT_ENABLE_PARALLEL_TRIANGULATION + << "dependency.tbb_version=" << TBB_VERSION_STRING << '\n' +#else + << "dependency.tbb_version=disabled\n" +#endif << "parallel_tds=" << CDT_ENABLE_PARALLEL_TRIANGULATION << '\n' + << "requested_threads=" << thread_count << '\n' + << "active_threads=" << active_threads << '\n' + << "fixture.id=generated-foliated-ball-v1\n" + << "random.seed=" << seed << '\n' + << "random.initialization_stream=" + << cdt::random_streams::initialization << '\n' + << "random.transition_stream=" << cdt::random_streams::transitions + << '\n' << "requested_simplices=" << simplices << '\n' << "timeslices=" << timeslices << '\n' << "generated_points=" << input.size() << '\n' + << "warmups=" << warmups << '\n' << "repetitions=" << repetitions << '\n' << "moves_per_repetition=" << move_count << '\n' << "final_vertices=" << final_vertices << '\n' << "final_cells=" << final_cells << '\n' + << "sample.unit=nanoseconds\n" << "checksum=" << checksum << '\n'; bulk_insert.print("bulk_insert"); foliation_repair.print("foliation_repair"); diff --git a/tests/CGAL_integration_test.cpp b/tests/CGAL_integration_test.cpp index 9514f3391..9ef033a4e 100644 --- a/tests/CGAL_integration_test.cpp +++ b/tests/CGAL_integration_test.cpp @@ -126,8 +126,9 @@ SCENARIO("CGAL insertion rejects ambiguous boundary metadata" * THEN("Pair construction rejects the narrowing conversion.") { - CHECK_THROWS_AS(cdt::foliated_triangulations::make_causal_vertices<3>( - points, timevalues), + CHECK_THROWS_AS(static_cast( + cdt::foliated_triangulations::make_causal_vertices<3>( + points, timevalues)), std::out_of_range); } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f571ad281..f5c83ea14 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -129,8 +129,9 @@ target_link_libraries( fmt::fmt-header-only spdlog::spdlog_header_only) -# Keep the issue #102 integration benchmark buildable without registering it -# as a correctness or performance test. Run it through `just benchmark-cgal`. +# Keep the issue #102/#88 integration benchmark buildable without registering +# it as a correctness or performance test. Run it through +# `just benchmark-cgal` or `just benchmark-cgal-parallel`. add_executable(CDT_cgal_benchmark CGAL_benchmark.cpp) target_compile_features(CDT_cgal_benchmark PRIVATE cxx_std_23) target_link_libraries( @@ -142,8 +143,6 @@ target_link_libraries( spdlog::spdlog_header_only CGAL::CGAL) -# Run unit tests -add_test(NAME cdt-unit-tests COMMAND $) if(ENABLE_PARALLEL_TRIANGULATION) add_test(NAME cdt-parallel-triangulation COMMAND $) @@ -160,5 +159,12 @@ elseif(ENABLE_COVERAGE) # the Codecov workflow's instrumentation-aware allowance. set(cdt_unit_test_timeout 600) endif() -set_tests_properties( - cdt-unit-tests PROPERTIES LABELS "unit" TIMEOUT "${cdt_unit_test_timeout}") + +# Discover each doctest case as an independent CTest process. Besides producing +# precise failure names, this prevents long-lived CGAL/oneTBB state from making +# the complete parallel-TDS suite exceed the per-test timeout. +include(doctest) +doctest_discover_tests( + CDT_unit_tests + TEST_PREFIX "unit: " + PROPERTIES LABELS "unit" TIMEOUT "${cdt_unit_test_timeout}") diff --git a/tests/Ergodic_moves_3_audit_test.cpp b/tests/Ergodic_moves_3_audit_test.cpp index 7fc5f01f2..a11a0d00c 100644 --- a/tests/Ergodic_moves_3_audit_test.cpp +++ b/tests/Ergodic_moves_3_audit_test.cpp @@ -129,7 +129,7 @@ namespace return result; } - [[nodiscard]] auto independent_cell_type(Cell_handle const cell) -> Cell_type + [[nodiscard]] auto independent_cell_type(Cell_handle const cell) -> CellType { std::array times{}; for (int index = 0; index < 4; ++index) @@ -137,13 +137,13 @@ namespace times.at(static_cast(index)) = cell->vertex(index)->info(); } auto const [minimum, maximum] = std::ranges::minmax(times); - if (maximum - minimum != 1) { return Cell_type::ACAUSAL; } + if (maximum - minimum != 1) { return CellType::ACAUSAL; } auto const lower_count = std::ranges::count(times, minimum); - if (lower_count == 3) { return Cell_type::THREE_ONE; } - if (lower_count == 2) { return Cell_type::TWO_TWO; } - if (lower_count == 1) { return Cell_type::ONE_THREE; } - return Cell_type::UNCLASSIFIED; + if (lower_count == 3) { return CellType::THREE_ONE; } + if (lower_count == 2) { return CellType::TWO_TWO; } + if (lower_count == 1) { return CellType::ONE_THREE; } + return CellType::UNCLASSIFIED; } void assign_independent_cell_metadata(Delaunay& triangulation) @@ -182,11 +182,11 @@ namespace { switch (independent_cell_type(cell)) { - case Cell_type::THREE_ONE: ++result.n3_31; break; - case Cell_type::TWO_TWO: ++result.n3_22; break; - case Cell_type::ONE_THREE: ++result.n3_13; break; - case Cell_type::ACAUSAL: - case Cell_type::UNCLASSIFIED: break; + case CellType::THREE_ONE: ++result.n3_31; break; + case CellType::TWO_TWO: ++result.n3_22; break; + case CellType::ONE_THREE: ++result.n3_13; break; + case CellType::ACAUSAL: + case CellType::UNCLASSIFIED: break; } } return result; @@ -403,8 +403,8 @@ namespace Cell_handle const cell_handle = cell; auto const type = independent_cell_type(cell); CHECK(triangulation.tds().is_cell(cell_handle)); - CHECK((type == Cell_type::THREE_ONE || type == Cell_type::TWO_TWO || - type == Cell_type::ONE_THREE)); + CHECK((type == CellType::THREE_ONE || type == CellType::TWO_TWO || + type == CellType::ONE_THREE)); CHECK_EQ(cell->info(), static_cast(type)); ++classified_cell_count; for (int index = 0; index < 4; ++index) @@ -436,8 +436,8 @@ namespace [[nodiscard]] auto make_23_fixture() -> Manifold { - static auto constexpr radius_2 = 2.0 * std::numbers::inv_sqrt3_v; - static auto constexpr sqrt_2 = std::numbers::sqrt2_v; + static constexpr auto radius_2 = 2.0 * std::numbers::inv_sqrt3_v; + static constexpr auto sqrt_2 = std::numbers::sqrt2_v; std::vector> vertices{ { 1, 0, 0}, { 0, 1, 0}, @@ -451,7 +451,7 @@ namespace [[nodiscard]] auto make_26_fixture() -> Manifold { - static auto constexpr radius_2 = 2.0 * std::numbers::inv_sqrt3_v; + static constexpr auto radius_2 = 2.0 * std::numbers::inv_sqrt3_v; std::vector> vertices{ { 0, 0, 0}, { 1, 0, 0}, @@ -465,7 +465,7 @@ namespace [[nodiscard]] auto make_44_fixture() -> Manifold { - static auto constexpr inverse_sqrt_2 = 1.0 / std::numbers::sqrt2_v; + static constexpr auto inverse_sqrt_2 = 1.0 / std::numbers::sqrt2_v; std::vector> vertices{ { 0, 0, 0}, { inverse_sqrt_2, 0, inverse_sqrt_2}, @@ -505,13 +505,13 @@ namespace if (first_time == second_time) { continue; } auto const cells = finite_incident_cells(triangulation, *edge); if (!cells || cells->size() != 3) { continue; } - auto const count = [&](Cell_type const type) { + auto const count = [&](CellType const type) { return std::ranges::count_if(*cells, [&](auto const cell) { return independent_cell_type(cell) == type; }); }; - if (count(Cell_type::TWO_TWO) == 2 && - count(Cell_type::THREE_ONE) + count(Cell_type::ONE_THREE) == 1) + if (count(CellType::TWO_TWO) == 2 && + count(CellType::THREE_ONE) + count(CellType::ONE_THREE) == 1) { return *edge; } @@ -530,12 +530,12 @@ namespace if (first_time != second_time) { continue; } auto const cells = finite_incident_cells(triangulation, *edge); if (!cells || cells->size() != 4) { continue; } - auto const count = [&](Cell_type const type) { + auto const count = [&](CellType const type) { return std::ranges::count_if(*cells, [&](auto const cell) { return independent_cell_type(cell) == type; }); }; - if (count(Cell_type::THREE_ONE) == 2 && count(Cell_type::ONE_THREE) == 2) + if (count(CellType::THREE_ONE) == 2 && count(CellType::ONE_THREE) == 2) { return *edge; } @@ -601,8 +601,8 @@ namespace } template - void check_stale_candidate(Result const& result, - move_tracker::move_type const expected_move) + void check_stale_candidate(Result const& result, + move_tracker::MoveType const expected_move) { REQUIRE_FALSE(result.has_value()); CHECK_EQ(result.error().reason(), @@ -718,7 +718,7 @@ SCENARIO("CDT move rejection is causal and failure-atomic" * auto triangulation = make_23_fixture().delaunay_snapshot(); auto two_two = foliated_triangulations::filter_cells<3>( foliated_triangulations::collect_cells<3>(triangulation), - Cell_type::TWO_TWO); + CellType::TWO_TWO); REQUIRE_EQ(two_two.size(), 1); REQUIRE(ergodic_moves::detail::try_23_move(triangulation, two_two.front())); assign_independent_cell_metadata(triangulation); @@ -770,21 +770,21 @@ SCENARIO("CDT move rejection is causal and failure-atomic" * auto const corrupt_metadata = [](Cell_handle const& cell) { auto const expected = foliated_triangulations::expected_cell_type<3>(cell); - cell->info() = static_cast(expected == Cell_type::TWO_TWO - ? Cell_type::THREE_ONE - : Cell_type::TWO_TWO); + cell->info() = static_cast(expected == CellType::TWO_TWO + ? CellType::THREE_ONE + : CellType::TWO_TWO); }; auto move_23 = make_23_fixture().delaunay_snapshot(); auto two_two = foliated_triangulations::filter_cells<3>( - foliated_triangulations::collect_cells<3>(move_23), Cell_type::TWO_TWO); + foliated_triangulations::collect_cells<3>(move_23), CellType::TWO_TWO); REQUIRE_EQ(two_two.size(), 1); corrupt_metadata(two_two.front()); auto const before_23 = canonical_triangulation(move_23); auto move_32 = make_23_fixture().delaunay_snapshot(); auto move_32_cells = foliated_triangulations::filter_cells<3>( - foliated_triangulations::collect_cells<3>(move_32), Cell_type::TWO_TWO); + foliated_triangulations::collect_cells<3>(move_32), CellType::TWO_TWO); REQUIRE_EQ(move_32_cells.size(), 1); REQUIRE(ergodic_moves::detail::try_23_move(move_32, move_32_cells.front())); assign_independent_cell_metadata(move_32); @@ -798,7 +798,7 @@ SCENARIO("CDT move rejection is causal and failure-atomic" * auto move_26 = make_26_fixture().delaunay_snapshot(); auto one_three = foliated_triangulations::filter_cells<3>( foliated_triangulations::collect_cells<3>(move_26), - Cell_type::ONE_THREE); + CellType::ONE_THREE); REQUIRE_EQ(one_three.size(), 1); corrupt_metadata(one_three.front()); auto const before_26 = canonical_triangulation(move_26); @@ -854,7 +854,7 @@ SCENARIO("CDT move rejection is causal and failure-atomic" * auto triangulation = make_23_fixture().delaunay_snapshot(); auto two_two = foliated_triangulations::filter_cells<3>( foliated_triangulations::collect_cells<3>(triangulation), - Cell_type::TWO_TWO); + CellType::TWO_TWO); REQUIRE_EQ(two_two.size(), 1); two_two.front()->vertex(0)->info() += 3; assign_independent_cell_metadata(triangulation); @@ -1055,17 +1055,26 @@ TEST_CASE("Seeded CDT move replay preserves invariants and inverse structure" * } } -TEST_CASE("Move validation rejects exact configuration-state drift" * - doctest::test_suite("ergodic-audit")) +SCENARIO("Move validation rejects exact configuration-state drift" * + doctest::test_suite("ergodic-audit")) { - auto const before = make_44_fixture(); - auto const after = Manifold{ - foliated_triangulations::FoliatedTriangulation_3{ - before.delaunay_snapshot(), -0.0, before.foliation_spacing()} - }; - REQUIRE(after.is_correct()); - CHECK_FALSE(ergodic_moves::detail::check_move( - before, after, move_tracker::move_type::FOUR_FOUR)); + GIVEN("Two valid manifolds that differ in exact configuration state") + { + auto const before = make_44_fixture(); + auto const after = Manifold{ + foliated_triangulations::FoliatedTriangulation_3{ + before.delaunay_snapshot(), -0.0, before.foliation_spacing()} + }; + REQUIRE(after.is_correct()); + + WHEN("The candidate is checked as a four-four move") + { + auto const valid = ergodic_moves::detail::check_move( + before, after, move_tracker::MoveType::FOUR_FOUR); + + THEN("The exact state mismatch is rejected") { CHECK_FALSE(valid); } + } + } } SCENARIO("Applicable moves carry validation evidence without CGAL handles" * @@ -1076,7 +1085,7 @@ SCENARIO("Applicable moves carry validation evidence without CGAL handles" * auto triangulation = make_23_fixture().delaunay_snapshot(); auto two_two = foliated_triangulations::filter_cells<3>( foliated_triangulations::collect_cells<3>(triangulation), - Cell_type::TWO_TWO); + CellType::TWO_TWO); REQUIRE_EQ(two_two.size(), 1); auto const prepared = ergodic_moves::detail::prepare_two_three( @@ -1100,7 +1109,7 @@ SCENARIO("Applicable moves carry validation evidence without CGAL handles" * THEN( "the stable locator reports a stale proof instead of dereferencing a handle") { - check_stale_candidate(executed, move_tracker::move_type::TWO_THREE); + check_stale_candidate(executed, move_tracker::MoveType::TWO_THREE); CHECK_EQ(canonical_triangulation(triangulation), before); } } @@ -1126,7 +1135,7 @@ SCENARIO("Applicable moves carry validation evidence without CGAL handles" * THEN("execution reports the originating (3,2) move as stale") { - check_stale_candidate(executed, move_tracker::move_type::THREE_TWO); + check_stale_candidate(executed, move_tracker::MoveType::THREE_TWO); CHECK_EQ(canonical_triangulation(triangulation), before); } } @@ -1136,7 +1145,7 @@ SCENARIO("Applicable moves carry validation evidence without CGAL handles" * auto triangulation = make_26_fixture().delaunay_snapshot(); auto one_three = foliated_triangulations::filter_cells<3>( foliated_triangulations::collect_cells<3>(triangulation), - Cell_type::ONE_THREE); + CellType::ONE_THREE); REQUIRE_EQ(one_three.size(), 1); auto const prepared = ergodic_moves::detail::prepare_two_six( triangulation, one_three.front()); @@ -1149,7 +1158,7 @@ SCENARIO("Applicable moves carry validation evidence without CGAL handles" * THEN("execution reports the originating (2,6) move as stale") { - check_stale_candidate(executed, move_tracker::move_type::TWO_SIX); + check_stale_candidate(executed, move_tracker::MoveType::TWO_SIX); CHECK_EQ(canonical_triangulation(triangulation), before); } } @@ -1181,7 +1190,7 @@ SCENARIO("Applicable moves carry validation evidence without CGAL handles" * THEN("execution reports the originating (6,2) move as stale") { - check_stale_candidate(executed, move_tracker::move_type::SIX_TWO); + check_stale_candidate(executed, move_tracker::MoveType::SIX_TWO); CHECK_EQ(canonical_triangulation(triangulation), before); } } @@ -1202,7 +1211,7 @@ SCENARIO("Applicable moves carry validation evidence without CGAL handles" * THEN("execution reports the originating (4,4) move as stale") { - check_stale_candidate(executed, move_tracker::move_type::FOUR_FOUR); + check_stale_candidate(executed, move_tracker::MoveType::FOUR_FOUR); CHECK_EQ(canonical_triangulation(triangulation), before); } } diff --git a/tests/Ergodic_moves_3_test.cpp b/tests/Ergodic_moves_3_test.cpp index a306f2265..8bb1770d6 100644 --- a/tests/Ergodic_moves_3_test.cpp +++ b/tests/Ergodic_moves_3_test.cpp @@ -19,11 +19,11 @@ using namespace cdt; using namespace std; using namespace manifolds; -static inline std::floating_point auto constexpr RADIUS_2 = +static inline constexpr std::floating_point auto RADIUS_2 = 2.0 * std::numbers::inv_sqrt3_v; -static inline std::floating_point auto constexpr SQRT_2 = +static inline constexpr std::floating_point auto SQRT_2 = std::numbers::sqrt2_v; -static inline std::floating_point auto constexpr INV_SQRT_2 = 1 / SQRT_2; +static inline constexpr std::floating_point auto INV_SQRT_2 = 1 / SQRT_2; namespace { @@ -41,10 +41,10 @@ namespace Int_precision n0; }; - [[nodiscard]] auto constexpr expected_geometry_delta( - move_tracker::move_type const move) -> ExpectedGeometryDelta + [[nodiscard]] constexpr auto expected_geometry_delta( + move_tracker::MoveType const move) -> ExpectedGeometryDelta { - using enum move_tracker::move_type; + using enum move_tracker::MoveType; switch (move) { case TWO_THREE: return {1, 0, 0, 0, 1, 2, 1, 1, 0, 0}; @@ -57,7 +57,7 @@ namespace } void check_geometry_delta(Manifold_3 const& before, Manifold_3 const& after, - move_tracker::move_type const move) + move_tracker::MoveType const move) { auto const expected = expected_geometry_delta(move); CHECK_EQ(after.N3() - before.N3(), expected.n3); @@ -148,9 +148,9 @@ SCENARIO("Use check_move to validate successful move" * THEN("check_move returns true") { check_geometry_delta(manifold_before, manifold, - move_tracker::move_type::TWO_THREE); + move_tracker::MoveType::TWO_THREE); CHECK(ergodic_moves::detail::check_move( - manifold_before, manifold, move_tracker::move_type::TWO_THREE)); + manifold_before, manifold, move_tracker::MoveType::TWO_THREE)); } } } @@ -205,9 +205,9 @@ SCENARIO( THEN("The move is correct and the manifold invariants are maintained") { check_geometry_delta(manifold_before, manifold, - move_tracker::move_type::TWO_THREE); + move_tracker::MoveType::TWO_THREE); CHECK(ergodic_moves::detail::check_move( - manifold_before, manifold, move_tracker::move_type::TWO_THREE)); + manifold_before, manifold, move_tracker::MoveType::TWO_THREE)); // Manual check REQUIRE(manifold.is_correct()); CHECK_EQ(manifold.vertices(), 5); @@ -263,9 +263,9 @@ SCENARIO( THEN("The move is correct and the manifold invariants are maintained") { check_geometry_delta(manifold_before, manifold, - move_tracker::move_type::THREE_TWO); + move_tracker::MoveType::THREE_TWO); CHECK(ergodic_moves::detail::check_move( - manifold_before, manifold, move_tracker::move_type::THREE_TWO)); + manifold_before, manifold, move_tracker::MoveType::THREE_TWO)); // Manual check REQUIRE(manifold.is_correct()); CHECK_EQ(manifold.vertices(), 5); @@ -344,9 +344,9 @@ SCENARIO( THEN("The move is correct and the manifold invariants are maintained") { check_geometry_delta(manifold_before, manifold, - move_tracker::move_type::TWO_SIX); + move_tracker::MoveType::TWO_SIX); CHECK(ergodic_moves::detail::check_move( - manifold_before, manifold, move_tracker::move_type::TWO_SIX)); + manifold_before, manifold, move_tracker::MoveType::TWO_SIX)); // Manual check REQUIRE(manifold.is_correct()); CHECK_EQ(manifold.vertices(), 6); // +1 vertex @@ -412,10 +412,10 @@ SCENARIO( THEN("The move is correct and the manifold invariants are maintained") { check_geometry_delta(manifold_before, manifold, - move_tracker::move_type::SIX_TWO); + move_tracker::MoveType::SIX_TWO); // Check the move CHECK(ergodic_moves::detail::check_move( - manifold_before, manifold, move_tracker::move_type::SIX_TWO)); + manifold_before, manifold, move_tracker::MoveType::SIX_TWO)); // Manual check REQUIRE(manifold.is_correct()); CHECK(manifold.is_foliated()); @@ -504,10 +504,10 @@ SCENARIO("Perform ergodic moves on the minimal manifold necessary (4,4) moves" * THEN("The move is correct and the manifold invariants are maintained") { check_geometry_delta(manifold_before, manifold, - move_tracker::move_type::FOUR_FOUR); + move_tracker::MoveType::FOUR_FOUR); // Check the move CHECK(ergodic_moves::detail::check_move( - manifold_before, manifold, move_tracker::move_type::FOUR_FOUR)); + manifold_before, manifold, move_tracker::MoveType::FOUR_FOUR)); CHECK_EQ(manifold.initial_radius(), manifold_before.initial_radius()); CHECK_EQ(manifold.foliation_spacing(), manifold_before.foliation_spacing()); diff --git a/tests/Foliated_triangulation_test.cpp b/tests/Foliated_triangulation_test.cpp index 7069cc0b5..e84963528 100644 --- a/tests/Foliated_triangulation_test.cpp +++ b/tests/Foliated_triangulation_test.cpp @@ -27,10 +27,10 @@ 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 = +static inline constexpr auto RADIUS_2 = 2.0 * std::numbers::inv_sqrt3_v; +static inline constexpr std::floating_point auto SQRT_2 = std::numbers::sqrt2_v; -static inline auto constexpr INV_SQRT_2 = 1.0 / SQRT_2; +static inline constexpr auto INV_SQRT_2 = 1.0 / SQRT_2; SCENARIO("FoliatedTriangulation special member and swap properties" * doctest::test_suite("foliated_triangulation")) @@ -153,7 +153,10 @@ SCENARIO("FoliatedTriangulation free functions" * WHEN("Causal vertices are created.") { THEN("An exception is thrown.") - { REQUIRE_THROWS(make_causal_vertices<3>(Vertices, Timevalues)); } + { + REQUIRE_THROWS( + static_cast(make_causal_vertices<3>(Vertices, Timevalues))); + } } } @@ -194,10 +197,10 @@ SCENARIO("FoliatedTriangulation free functions" * "A minimal triangulation with non-default initial radius and radial " "separation.") { - auto constexpr desired_simplices = 2; - auto constexpr desired_timeslices = 2; - auto constexpr initial_radius = 3.0; - auto constexpr foliation_spacing = 2.0; + constexpr auto desired_simplices = 2; + constexpr auto desired_timeslices = 2; + constexpr auto initial_radius = 3.0; + constexpr auto foliation_spacing = 2.0; FoliatedTriangulation_3 const triangulation( desired_simplices, desired_timeslices, cdt::Random{92}, initial_radius, foliation_spacing); @@ -379,8 +382,8 @@ SCENARIO("FoliatedTriangulation_3 initialization" * } WHEN("Constructing the minimum triangulation.") { - auto constexpr desired_simplices = 2; - auto constexpr desired_timeslices = 2; + constexpr auto desired_simplices = 2; + constexpr auto desired_timeslices = 2; FoliatedTriangulation_3 triangulation( desired_simplices, desired_timeslices, cdt::Random{92}); THEN("Triangulation is valid and foliated.") @@ -410,10 +413,10 @@ SCENARIO("FoliatedTriangulation_3 initialization" * "Constructing the minimal triangulation with non-default initial " "radius and separation.") { - auto constexpr desired_simplices = 2; - auto constexpr desired_timeslices = 2; - auto constexpr initial_radius = 3.0; - auto constexpr radial_factor = 2.0; + constexpr auto desired_simplices = 2; + constexpr auto desired_timeslices = 2; + constexpr auto initial_radius = 3.0; + constexpr auto radial_factor = 2.0; FoliatedTriangulation_3 const triangulation( desired_simplices, desired_timeslices, cdt::Random{92}, initial_radius, radial_factor); @@ -429,10 +432,10 @@ SCENARIO("FoliatedTriangulation_3 initialization" * "Constructing a small triangulation with fractional initial radius and " "separation.") { - auto constexpr desired_simplices = 24; - auto constexpr desired_timeslices = 3; - auto constexpr initial_radius = 1.5; - auto constexpr radial_factor = 1.1; + constexpr auto desired_simplices = 24; + constexpr auto desired_timeslices = 3; + constexpr auto initial_radius = 1.5; + constexpr auto radial_factor = 1.1; FoliatedTriangulation_3 const triangulation( desired_simplices, desired_timeslices, cdt::Random{92}, initial_radius, radial_factor); @@ -446,8 +449,8 @@ SCENARIO("FoliatedTriangulation_3 initialization" * } WHEN("Constructing a medium triangulation.") { - auto constexpr desired_simplices = 6400; - auto constexpr desired_timeslices = 7; + constexpr auto desired_simplices = 6400; + constexpr auto desired_timeslices = 7; FoliatedTriangulation_3 const triangulation( desired_simplices, desired_timeslices, cdt::Random{92}); THEN("Triangulation is valid and foliated.") @@ -471,15 +474,15 @@ SCENARIO("FoliatedTriangulation_3 initialization" * CHECK_GT(triangulation.max_time(), triangulation.min_time()); auto snapshot = triangulation.delaunay_snapshot(); auto edges = collect_edges<3>(snapshot); - auto timelike_edges = filter_edges<3>(edges, true); - auto spacelike_edges = filter_edges<3>(edges, false); + auto timelike_edges = filter_edges<3>(edges, EdgeType::TIMELIKE); + auto spacelike_edges = filter_edges<3>(edges, EdgeType::SPACELIKE); auto check_timelike = [](Edge_handle_t<3> const& edge) { - CHECK(classify_edge<3>(edge)); + CHECK_EQ(classify_edge<3>(edge), EdgeType::TIMELIKE); }; ranges::for_each(timelike_edges, check_timelike); auto check_spacelike = [](Edge_handle_t<3> const& edge) { - CHECK(!classify_edge<3>(edge)); + CHECK_EQ(classify_edge<3>(edge), EdgeType::SPACELIKE); }; ranges::for_each(spacelike_edges, check_spacelike); } @@ -493,8 +496,8 @@ SCENARIO("FoliatedTriangulation_3 copying" * spdlog::debug("FoliatedTriangulation_3 copying.\n"); GIVEN("A FoliatedTriangulation_3") { - auto constexpr desired_simplices = 6400; - auto constexpr desired_timeslices = 7; + constexpr auto desired_simplices = 6400; + constexpr auto desired_timeslices = 7; FoliatedTriangulation_3 triangulation(desired_simplices, desired_timeslices, cdt::Random{92}); WHEN("It is copied") @@ -508,6 +511,7 @@ SCENARIO("FoliatedTriangulation_3 copying" * } THEN("The foliated triangulations have identical properties.") { + CHECK_EQ(triangulation.delaunay_snapshot(), ft2.delaunay_snapshot()); CHECK_EQ(triangulation.is_initialized(), ft2.is_initialized()); CHECK_EQ(triangulation.number_of_finite_cells(), ft2.number_of_finite_cells()); @@ -530,8 +534,8 @@ SCENARIO("FoliatedTriangulation_3 moving" * { GIVEN("A foliated triangulation with known simplex classifications.") { - auto constexpr desired_simplices = 64; - auto constexpr desired_timeslices = 4; + constexpr auto desired_simplices = 64; + constexpr auto desired_timeslices = 4; FoliatedTriangulation_3 source(desired_simplices, desired_timeslices, cdt::Random{92}); auto const expected_cells = source.number_of_finite_cells(); @@ -608,7 +612,7 @@ SCENARIO("Detecting and fixing problems with vertices and cells" * { auto snapshot = triangulation.delaunay_snapshot(); CHECK(triangulation.is_correct()); - CHECK_FALSE(check_timevalues<3>(snapshot)); + CHECK(has_valid_timevalues<3>(snapshot)); } THEN("No errors in the triangulation foliation are detected") { @@ -636,7 +640,11 @@ SCENARIO("Detecting and fixing problems with vertices and cells" * THEN("Construction synchronizes the replacement without mutation.") { CHECK(replacement.is_initialized()); + CHECK(replacement.check_all_vertices()); + CHECK(replacement.check_all_cells()); CHECK(triangulation.is_initialized()); + CHECK(triangulation.check_all_vertices()); + CHECK(triangulation.check_all_cells()); } } } @@ -659,7 +667,11 @@ SCENARIO("Detecting and fixing problems with vertices and cells" * THEN("Construction synchronizes the replacement without mutation.") { CHECK(replacement.is_initialized()); + CHECK(replacement.check_all_vertices()); + CHECK(replacement.check_all_cells()); CHECK(triangulation.is_initialized()); + CHECK(triangulation.check_all_vertices()); + CHECK(triangulation.check_all_cells()); } } } @@ -681,6 +693,8 @@ SCENARIO("Detecting and fixing problems with vertices and cells" * { CHECK_FALSE(triangulation.fix_vertices()); CHECK(triangulation.is_initialized()); + CHECK(triangulation.check_all_vertices()); + CHECK(triangulation.check_all_cells()); } } WHEN("Constructing a triangulation with an incorrect low value vertex.") @@ -698,6 +712,8 @@ SCENARIO("Detecting and fixing problems with vertices and cells" * { CHECK_FALSE(triangulation.fix_vertices()); CHECK(triangulation.is_initialized()); + CHECK(triangulation.check_all_vertices()); + CHECK(triangulation.check_all_cells()); } } WHEN( @@ -717,12 +733,16 @@ SCENARIO("Detecting and fixing problems with vertices and cells" * { CHECK_FALSE(triangulation.fix_vertices()); CHECK(triangulation.is_initialized()); + CHECK(triangulation.check_all_vertices()); + CHECK(triangulation.check_all_cells()); } AND_THEN("The cell type is correct.") { CHECK_FALSE(triangulation.fix_vertices()); CHECK_FALSE(triangulation.fix_cells()); CHECK(triangulation.is_initialized()); + CHECK(triangulation.check_all_vertices()); + CHECK(triangulation.check_all_cells()); } } WHEN( @@ -742,7 +762,7 @@ SCENARIO("Detecting and fixing problems with vertices and cells" * CHECK_FALSE(triangulation.is_initialized()); auto snapshot = triangulation.delaunay_snapshot(); auto cell = snapshot.finite_cells_begin(); - CHECK_EQ(expected_cell_type<3>(cell), Cell_type::ACAUSAL); + CHECK_EQ(expected_cell_type<3>(cell), CellType::ACAUSAL); } } WHEN("Constructing a triangulation with an unfixable vertex.") @@ -765,16 +785,18 @@ SCENARIO("Detecting and fixing problems with vertices and cells" * FoliatedTriangulation_3 triangulation(delaunay_triangulation); THEN("The incorrect cell can be identified.") { - auto bad_cells = check_timevalues<3>(delaunay_triangulation); - CHECK_MESSAGE(bad_cells.has_value(), "No bad cells found."); + auto bad_cells = + find_invalid_timevalue_cells<3>(delaunay_triangulation); + CHECK_FALSE_MESSAGE(bad_cells.empty(), "No bad cells found."); } AND_THEN("The incorrect vertex can be identified.") { - auto bad_cells = check_timevalues<3>(delaunay_triangulation); - CHECK_MESSAGE(bad_cells.has_value(), "No bad cells found."); - if (bad_cells) + auto bad_cells = + find_invalid_timevalue_cells<3>(delaunay_triangulation); + CHECK_FALSE_MESSAGE(bad_cells.empty(), "No bad cells found."); + if (!bad_cells.empty()) { - auto bad_vertex = find_bad_vertex<3>(bad_cells->front()); + auto bad_vertex = find_bad_vertex<3>(bad_cells.front()); CHECK_EQ(bad_vertex->info(), 3); } } diff --git a/tests/Function_ref_test.cpp b/tests/Function_ref_test.cpp index 256b98d87..54795832e 100644 --- a/tests/Function_ref_test.cpp +++ b/tests/Function_ref_test.cpp @@ -26,9 +26,9 @@ namespace { [[nodiscard]] auto make_23_move_manifold() -> Manifold_3 { - auto constexpr radius_2 = 2.0 * std::numbers::inv_sqrt3_v; - auto constexpr sqrt_2 = std::numbers::sqrt2_v; - auto const vertices = std::array{ + constexpr auto radius_2 = 2.0 * std::numbers::inv_sqrt3_v; + constexpr auto sqrt_2 = std::numbers::sqrt2_v; + auto const vertices = std::array{ Point_t<3>{ 1, 0, 0}, Point_t<3>{ 0, 1, 0}, Point_t<3>{ 0, 0, 1}, @@ -43,7 +43,7 @@ namespace SCENARIO("Simple Lambda operations" * doctest::test_suite("function_ref")) { - auto constexpr increment_lambda = [](int a) { return ++a; }; // NOLINT + constexpr auto increment_lambda = [](int a) { return ++a; }; // NOLINT GIVEN("A simple lambda.") { WHEN("Lambda is called with 0.") @@ -81,7 +81,7 @@ SCENARIO("Complex lambda operations" * doctest::test_suite("function_ref")) CAPTURE(random.seed()); auto result = move23(manifold, random); CHECK(ergodic_moves::detail::check_move( - manifold_before, result, move_tracker::move_type::TWO_THREE)); + manifold_before, result, move_tracker::MoveType::TWO_THREE)); } } } @@ -116,7 +116,7 @@ SCENARIO("Function_ref operations" * doctest::test_suite("function_ref")) { CHECK(ergodic_moves::detail::check_move( manifold_before, result.value(), - move_tracker::move_type::TWO_THREE)); + move_tracker::MoveType::TWO_THREE)); } } } @@ -140,7 +140,7 @@ SCENARIO("Function_ref operations" * doctest::test_suite("function_ref")) "correct.") { CHECK(ergodic_moves::detail::check_move( - manifold_before, result, move_tracker::move_type::TWO_THREE)); + manifold_before, result, move_tracker::MoveType::TWO_THREE)); } } } diff --git a/tests/Geometry_test.cpp b/tests/Geometry_test.cpp index 57793151d..dfc8f8b95 100644 --- a/tests/Geometry_test.cpp +++ b/tests/Geometry_test.cpp @@ -74,8 +74,8 @@ SCENARIO("3-Geometry classification" * doctest::test_suite("geometry")) { WHEN("It is constructed with a Delaunay triangulation.") { - auto constexpr desired_simplices = 72; - auto constexpr desired_timeslices = 3; + constexpr auto desired_simplices = 72; + constexpr auto desired_timeslices = 3; FoliatedTriangulation_3 const triangulation( desired_simplices, desired_timeslices, cdt::Random{92}); Geometry_3 geometry(triangulation); @@ -116,7 +116,7 @@ SCENARIO("3-Geometry initialization" * doctest::test_suite("geometry")) { THEN("All data members are zero-initialized.") { - Geometry_3 constexpr geometry; + constexpr Geometry_3 geometry; REQUIRE_EQ(geometry.N3, 0); REQUIRE_EQ(geometry.N3_31, 0); REQUIRE_EQ(geometry.N3_13, 0); @@ -130,8 +130,8 @@ SCENARIO("3-Geometry initialization" * doctest::test_suite("geometry")) } WHEN("It is constructed with a triangulation.") { - auto constexpr desired_simplices = 640; - auto constexpr desired_timeslices = 4; + constexpr auto desired_simplices = 640; + constexpr auto desired_timeslices = 4; FoliatedTriangulation_3 const triangulation( desired_simplices, desired_timeslices, cdt::Random{92}); Geometry_3 const geometry(triangulation); diff --git a/tests/Manifold_test.cpp b/tests/Manifold_test.cpp index 97bca495f..1eae2de17 100644 --- a/tests/Manifold_test.cpp +++ b/tests/Manifold_test.cpp @@ -23,10 +23,13 @@ using namespace manifolds; static_assert(std::is_nothrow_swappable_v); static_assert(std::is_nothrow_move_constructible_v); static_assert(std::is_nothrow_move_assignable_v); +static_assert( + std::is_same_v().geometry()), + Geometry_3 const&>); using Causal_vertices_3_t = Causal_vertices_t<3>; -static inline auto constexpr RADIUS_2 = 2.0 * std::numbers::inv_sqrt3_v; +static inline constexpr auto RADIUS_2 = 2.0 * std::numbers::inv_sqrt3_v; SCENARIO("Manifold special member and swap properties" * doctest::test_suite("manifold")) @@ -145,8 +148,8 @@ SCENARIO("Manifold free functions" * doctest::test_suite("manifold")) { THEN("An exception is thrown.") { - REQUIRE_THROWS( - manifolds::make_causal_vertices<3>(Vertices, Timevalues)); + REQUIRE_THROWS(static_cast( + manifolds::make_causal_vertices<3>(Vertices, Timevalues))); } } } @@ -265,12 +268,6 @@ SCENARIO("3-Manifold initialization" * doctest::test_suite("manifold")) REQUIRE(manifold.is_delaunay()); REQUIRE(manifold.is_valid()); } - THEN("The geometry is of type geometry class.") - { - auto const& geometry_type = typeid(manifold.get_geometry()).name(); - std::string geometry_string{geometry_type}; - CHECK_NE(geometry_string.find("Geometry"), std::string::npos); - } } WHEN("It is constructed from causal vertices.") { @@ -286,12 +283,6 @@ SCENARIO("3-Manifold initialization" * doctest::test_suite("manifold")) REQUIRE(manifold.is_delaunay()); REQUIRE(manifold.is_valid()); } - THEN("The geometry is of type geometry class.") - { - auto const& geometry_type = typeid(manifold.get_geometry()).name(); - std::string geometry_string{geometry_type}; - CHECK_NE(geometry_string.find("Geometry"), std::string::npos); - } THEN("The geometry matches the triangulation.") { REQUIRE(manifold.is_foliated()); @@ -327,12 +318,6 @@ SCENARIO("3-Manifold initialization" * doctest::test_suite("manifold")) REQUIRE(manifold.is_delaunay()); REQUIRE(manifold.is_valid()); } - THEN("The geometry is of type geometry class.") - { - auto const& geometry_type = typeid(manifold.get_geometry()).name(); - std::string geometry_string{geometry_type}; - CHECK_NE(geometry_string.find("Geometry"), std::string::npos); - } THEN("The geometry matches the triangulation.") { REQUIRE(manifold.is_foliated()); @@ -352,8 +337,8 @@ SCENARIO("3-Manifold initialization" * doctest::test_suite("manifold")) } WHEN("Constructing the minimum size triangulation.") { - auto constexpr desired_simplices = 2; - auto constexpr desired_timeslices = 2; + constexpr auto desired_simplices = 2; + constexpr auto desired_timeslices = 2; Manifold_3 const manifold(desired_simplices, desired_timeslices, cdt::Random{92}); THEN("Triangulation is valid.") { REQUIRE(manifold.is_correct()); } @@ -379,8 +364,8 @@ SCENARIO("3-Manifold initialization" * doctest::test_suite("manifold")) } WHEN("Constructing a small triangulation.") { - auto constexpr desired_simplices = 640; - auto constexpr desired_timeslices = 4; + constexpr auto desired_simplices = 640; + constexpr auto desired_timeslices = 4; Manifold_3 const manifold(desired_simplices, desired_timeslices, cdt::Random{92}); THEN("Triangulation is valid.") { REQUIRE(manifold.is_correct()); } @@ -395,8 +380,8 @@ SCENARIO("3-Manifold initialization" * doctest::test_suite("manifold")) } WHEN("Constructing a medium triangulation.") { - auto constexpr desired_simplices = 6400; - auto constexpr desired_timeslices = 7; + constexpr auto desired_simplices = 6400; + constexpr auto desired_timeslices = 7; Manifold_3 const manifold(desired_simplices, desired_timeslices, cdt::Random{92}); THEN("Triangulation is valid.") { REQUIRE(manifold.is_correct()); } @@ -434,22 +419,22 @@ SCENARIO("3-Manifold function checks" * doctest::test_suite("manifold")) { WHEN("It is initialized.") { - auto constexpr desired_timeslices = 4; - auto constexpr desired_simplices = 640; + constexpr auto desired_timeslices = 4; + constexpr auto desired_simplices = 640; 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); - CHECK_EQ(manifold.N3_31(), manifold.get_geometry().N3_31); - CHECK_EQ(manifold.N3_13(), manifold.get_geometry().N3_13); - CHECK_EQ(manifold.N3_31_13(), manifold.get_geometry().N3_31_13); - CHECK_EQ(manifold.N3_22(), manifold.get_geometry().N3_22); - CHECK_EQ(manifold.N2(), manifold.get_geometry().N2); - CHECK_EQ(manifold.N1(), manifold.get_geometry().N1); - CHECK_EQ(manifold.N1_TL(), manifold.get_geometry().N1_TL); - CHECK_EQ(manifold.N1_SL(), manifold.get_geometry().N1_SL); - CHECK_EQ(manifold.N0(), manifold.get_geometry().N0); + CHECK_EQ(manifold.N3(), manifold.geometry().N3); + CHECK_EQ(manifold.N3_31(), manifold.geometry().N3_31); + CHECK_EQ(manifold.N3_13(), manifold.geometry().N3_13); + CHECK_EQ(manifold.N3_31_13(), manifold.geometry().N3_31_13); + CHECK_EQ(manifold.N3_22(), manifold.geometry().N3_22); + CHECK_EQ(manifold.N2(), manifold.geometry().N2); + CHECK_EQ(manifold.N1(), manifold.geometry().N1); + CHECK_EQ(manifold.N1_TL(), manifold.geometry().N1_TL); + CHECK_EQ(manifold.N1_SL(), manifold.geometry().N1_SL); + CHECK_EQ(manifold.N0(), manifold.geometry().N0); } } } @@ -459,8 +444,8 @@ SCENARIO("3-Manifold copying" * doctest::test_suite("manifold")) spdlog::debug("3-Manifold copying.\n"); GIVEN("A 3-manifold.") { - auto constexpr desired_simplices = 640; - auto constexpr desired_timeslices = 4; + constexpr auto desired_simplices = 640; + constexpr auto desired_timeslices = 4; Manifold_3 manifold(desired_simplices, desired_timeslices, cdt::Random{92}); WHEN("It is copied.") { @@ -495,8 +480,8 @@ SCENARIO("3-Manifold moving" * doctest::test_suite("manifold")) { GIVEN("A 3-manifold with known geometry.") { - auto constexpr desired_simplices = 64; - auto constexpr desired_timeslices = 4; + constexpr auto desired_simplices = 64; + constexpr auto desired_timeslices = 4; Manifold_3 source(desired_simplices, desired_timeslices, cdt::Random{92}); auto const expected_simplices = source.simplices(); auto const expected_faces = source.faces(); @@ -554,8 +539,8 @@ SCENARIO("3-Manifold value rebuild" * doctest::test_suite("manifold")) spdlog::debug("3-Manifold update geometry.\n"); GIVEN("A 3-manifold.") { - auto constexpr desired_simplices = 640; - auto constexpr desired_timeslices = 4; + constexpr auto desired_simplices = 640; + constexpr auto desired_timeslices = 4; Manifold_3 manifold(desired_simplices, desired_timeslices, cdt::Random{92}); WHEN("We rebuild it as a new value.") { @@ -585,8 +570,8 @@ SCENARIO("3-Manifold mutation" * doctest::test_suite("manifold")) spdlog::debug("3-Manifold mutation.\n"); GIVEN("A pair of 3-manifolds.") { - auto constexpr desired_simplices = 640; - auto constexpr desired_timeslices = 4; + constexpr auto desired_simplices = 640; + constexpr auto desired_timeslices = 4; Manifold_3 manifold1(desired_simplices, desired_timeslices, cdt::Random{92}); Manifold_3 const manifold2(desired_simplices, desired_timeslices, @@ -685,8 +670,8 @@ SCENARIO("3-Manifold validation and fixing" * doctest::test_suite("manifold")) { WHEN("It is constructed.") { - auto constexpr desired_timeslices = 7; - auto constexpr desired_simplices = 6400; + constexpr auto desired_timeslices = 7; + constexpr auto desired_simplices = 6400; Manifold_3 const manifold(desired_simplices, desired_timeslices, cdt::Random{92}); THEN("The triangulation is valid and Delaunay.") diff --git a/tests/Metropolis_test.cpp b/tests/Metropolis_test.cpp index c19d72390..ec0119adb 100644 --- a/tests/Metropolis_test.cpp +++ b/tests/Metropolis_test.cpp @@ -29,9 +29,9 @@ 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{ + constexpr auto radius = 2.0 * std::numbers::inv_sqrt3_v; + constexpr auto 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}, @@ -44,8 +44,8 @@ namespace [[nodiscard]] auto minimal_26_manifold() -> Manifold_3 { - auto constexpr radius = 2.0 * std::numbers::inv_sqrt3_v; - vector vertices{ + constexpr auto 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}, @@ -57,7 +57,7 @@ namespace } [[nodiscard]] auto actual_raw_site_count(Manifold_3 const& manifold, - move_tracker::move_type const move) + move_tracker::MoveType const move) -> Int_precision { auto triangulation = manifold.delaunay_snapshot(); @@ -65,41 +65,43 @@ namespace return static_cast(sites.size()); }; - using enum move_tracker::move_type; + using enum move_tracker::MoveType; switch (move) { case TWO_THREE: return count(foliated_triangulations::filter_cells<3>( foliated_triangulations::collect_cells<3>(triangulation), - Cell_type::TWO_TWO)); + CellType::TWO_TWO)); case THREE_TWO: return count(foliated_triangulations::filter_edges<3>( - foliated_triangulations::collect_edges<3>(triangulation), true)); + foliated_triangulations::collect_edges<3>(triangulation), + EdgeType::TIMELIKE)); case TWO_SIX: return count(foliated_triangulations::filter_cells<3>( foliated_triangulations::collect_cells<3>(triangulation), - Cell_type::ONE_THREE)); + CellType::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)); + foliated_triangulations::collect_edges<3>(triangulation), + EdgeType::SPACELIKE)); } return 0; } - void check_proposal_domain(Manifold_3 const& manifold, - move_tracker::move_type const move) + void check_proposal_domain(Manifold_3 const& manifold, + move_tracker::MoveType 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), + CHECK_EQ(Metropolis_3::proposal_site_count(manifold.geometry(), move), actual); auto const expected = 1.0L / (5.0L * static_cast(actual)); auto const observed = - Metropolis_3::proposal_probability(manifold.get_geometry(), move); + Metropolis_3::proposal_probability(manifold.geometry(), move); CHECK(mpfr_values::to_long_double(observed) == doctest::Approx(expected)); } @@ -113,10 +115,10 @@ namespace explicit CountingGenerator(std::uint64_t const seed) : m_engine{seed} {} - [[nodiscard]] static auto constexpr min() noexcept -> result_type + [[nodiscard]] static constexpr auto min() noexcept -> result_type { return std::mt19937_64::min(); } - [[nodiscard]] static auto constexpr max() noexcept -> result_type + [[nodiscard]] static constexpr auto max() noexcept -> result_type { return std::mt19937_64::max(); } auto operator()() -> result_type @@ -186,11 +188,8 @@ SCENARIO("MoveStrategy special member and swap properties" * REQUIRE(is_nothrow_destructible_v); spdlog::debug("It is no-throw destructible.\n"); } - THEN("It is default constructible.") - { - REQUIRE(is_default_constructible_v); - spdlog::debug("It is default constructible.\n"); - } + THEN("It cannot be constructed without physical parameters.") + { REQUIRE_FALSE(is_default_constructible_v); } THEN("It is no-throw copy constructible.") { REQUIRE(is_nothrow_copy_constructible_v); @@ -225,7 +224,10 @@ SCENARIO("MoveStrategy special member and swap properties" * bool>); REQUIRE(is_constructible_v); + bool, cdt::RandomSeed>); + REQUIRE_FALSE(is_constructible_v); spdlog::debug("Its file-output policy is configurable.\n"); } } @@ -234,56 +236,57 @@ SCENARIO("MoveStrategy special member and swap properties" * SCENARIO("Metropolis member functions" * doctest::test_suite("metropolis")) { - auto constexpr Alpha = static_cast(0.6); - auto constexpr K = static_cast(1.1); // NOLINT - auto constexpr Lambda = static_cast(0.1); + constexpr auto Alpha = static_cast(0.6); + constexpr auto K = static_cast(1.1); // NOLINT + constexpr auto Lambda = static_cast(0.1); GIVEN("A correctly-constructed Manifold_3.") { - auto constexpr simplices = 640; - auto constexpr timeslices = 4; - auto constexpr output_every_n_passes = 1; - auto constexpr passes = 10; + constexpr auto simplices = 640; + constexpr auto timeslices = 4; + constexpr auto output_every_n_passes = 1; + constexpr auto passes = 10; 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, - true, 92); + true, cdt::RandomSeed{92}); THEN("The Metropolis function object is initialized correctly.") { - CHECK_EQ(testrun.Alpha(), Alpha); - CHECK_EQ(testrun.K(), K); - CHECK_EQ(testrun.Lambda(), Lambda); + CHECK_EQ(testrun.alpha(), Alpha); + CHECK_EQ(testrun.k(), K); + CHECK_EQ(testrun.lambda(), Lambda); CHECK_EQ(testrun.passes(), passes); CHECK_EQ(testrun.checkpoint(), output_every_n_passes); CHECK(testrun.writes_files()); - 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); + CHECK_EQ(testrun.proposed().total(), 0); + CHECK_EQ(testrun.accepted().total(), 0); + CHECK_EQ(testrun.rejected().total(), 0); + CHECK_EQ(testrun.attempted().total(), 0); + CHECK_EQ(testrun.succeeded().total(), 0); + CHECK_EQ(testrun.failed().total(), 0); } 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, 92); + output_every_n_passes, false, + cdt::RandomSeed{92}); CHECK_EQ(no_file_output_run.checkpoint(), output_every_n_passes); CHECK_FALSE(no_file_output_run.writes_files()); } THEN("Initialization reads the canonical geometry without making moves.") { 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); + CHECK_EQ(testrun.geometry().N1_TL, universe.N1_TL()); + CHECK_EQ(testrun.geometry().N3_31_13, universe.N3_31_13()); + CHECK_EQ(testrun.geometry().N3_22, universe.N3_22()); + CHECK_EQ(testrun.proposed().total(), 0); + CHECK_EQ(testrun.accepted().total(), 0); + CHECK_EQ(testrun.rejected().total(), 0); + CHECK_EQ(testrun.attempted().total(), 0); + CHECK_EQ(testrun.succeeded().total(), 0); + CHECK_EQ(testrun.failed().total(), 0); } } WHEN("A nonpositive pass or checkpoint count is supplied.") @@ -291,12 +294,14 @@ 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, true, 92), + Metropolis_3(Alpha, K, Lambda, -1, output_every_n_passes, true, + cdt::RandomSeed{92}), std::invalid_argument); - CHECK_THROWS_AS( - 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, true, 92), + CHECK_THROWS_AS(Metropolis_3(Alpha, K, Lambda, 0, output_every_n_passes, + true, cdt::RandomSeed{92}), + std::invalid_argument); + CHECK_THROWS_AS(Metropolis_3(Alpha, K, Lambda, passes, 0, true, + cdt::RandomSeed{92}), std::invalid_argument); } } @@ -304,12 +309,14 @@ 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, true, 92), - std::domain_error); + CHECK_THROWS_AS( + Metropolis_3(0.5L, K, Lambda, passes, output_every_n_passes, true, + cdt::RandomSeed{92}), + std::domain_error); CHECK_THROWS_AS( Metropolis_3(std::numeric_limits::infinity(), K, - Lambda, passes, output_every_n_passes, true, 92), + Lambda, passes, output_every_n_passes, true, + cdt::RandomSeed{92}), std::invalid_argument); } } @@ -319,8 +326,8 @@ SCENARIO("Metropolis member functions" * doctest::test_suite("metropolis")) 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); + constexpr auto Alpha = 0.6L; + Metropolis_3 strategy(Alpha, 0.0L, 0.0L, 1, 1, false, cdt::RandomSeed{17}); GIVEN("A small pair of states connected by a (2,3) move.") { @@ -331,10 +338,10 @@ SCENARIO("Metropolis-Hastings proposal and acceptance ratios" * 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 forward = Metropolis_3::hastings_ratio( + current, proposed, move_tracker::MoveType::TWO_THREE); + auto const reverse = Metropolis_3::hastings_ratio( + proposed, current, move_tracker::MoveType::THREE_TWO); auto const round_trip = mpfr_values::multiply(forward, reverse); THEN("They are the exact reverse-to-forward ratios.") @@ -346,7 +353,7 @@ SCENARIO("Metropolis-Hastings proposal and acceptance ratios" * 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); + current, proposed, move_tracker::MoveType::TWO_THREE); CHECK(mpfr_values::to_long_double(probability) == doctest::Approx(0.4L)); } @@ -362,10 +369,10 @@ SCENARIO("Metropolis-Hastings proposal and acceptance ratios" * 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); + auto const forward = Metropolis_3::hastings_ratio( + current, proposed, move_tracker::MoveType::TWO_SIX); + auto const reverse = Metropolis_3::hastings_ratio( + proposed, current, move_tracker::MoveType::SIX_TWO); CHECK(mpfr_values::to_long_double(forward) == doctest::Approx(3.0L / 8.0L)); CHECK(mpfr_values::to_long_double(reverse) == @@ -381,11 +388,25 @@ SCENARIO("Metropolis-Hastings proposal and acceptance ratios" * 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); + auto const ratio = Metropolis_3::hastings_ratio( + current, proposed, move_tracker::MoveType::FOUR_FOUR); CHECK(mpfr_values::to_long_double(ratio) == doctest::Approx(1.0L)); } } + + GIVEN("An unrecognized move enumeration value.") + { + auto const unknown = static_cast(255); + Geometry_3 geometry; + + THEN("No inverse move is fabricated.") + { + CHECK_FALSE(Metropolis_3::reverse_move(unknown).has_value()); + CHECK_THROWS_AS(static_cast(Metropolis_3::hastings_ratio( + geometry, geometry, unknown)), + std::invalid_argument); + } + } } SCENARIO("Metropolis proposal domains match the sampled raw sites" * @@ -399,28 +420,26 @@ SCENARIO("Metropolis proposal domains match the sampled raw sites" * THEN("Every declared site count matches an independent enumeration.") { CHECK_EQ(actual_raw_site_count(two_three_state, - move_tracker::move_type::TWO_THREE), + move_tracker::MoveType::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), + move_tracker::MoveType::THREE_TWO), 5); + CHECK_EQ( + actual_raw_site_count(two_six_state, move_tracker::MoveType::TWO_SIX), + 1); + CHECK_EQ( + actual_raw_site_count(two_six_state, move_tracker::MoveType::SIX_TWO), + 5); CHECK_EQ(actual_raw_site_count(two_six_state, - move_tracker::move_type::FOUR_FOUR), + move_tracker::MoveType::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); + check_proposal_domain(two_three_state, move_tracker::MoveType::TWO_THREE); + check_proposal_domain(two_three_state, move_tracker::MoveType::THREE_TWO); + check_proposal_domain(two_six_state, move_tracker::MoveType::TWO_SIX); + check_proposal_domain(two_six_state, move_tracker::MoveType::SIX_TWO); + check_proposal_domain(two_six_state, move_tracker::MoveType::FOUR_FOUR); } } @@ -429,7 +448,7 @@ SCENARIO("Metropolis proposal domains match the sampled raw sites" * 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}; + constexpr auto draws = std::size_t{50'000}; WHEN("The production one-site selector is sampled repeatedly.") { @@ -443,8 +462,8 @@ SCENARIO("Metropolis proposal domains match the sampled raw sites" * THEN("All sites remain within a conservative uniformity envelope.") { - auto constexpr expected = draws / sites.size(); - auto constexpr tolerance = expected / 10; + constexpr auto expected = draws / sites.size(); + constexpr auto tolerance = expected / 10; for (auto const selected : selections) { CHECK_GE(selected, expected - tolerance); @@ -510,34 +529,34 @@ SCENARIO("The (6,2) proposal uses the caller-owned generator throughout" * SCENARIO("Metropolis transitions are sequential and failure-aware" * doctest::test_suite("metropolis")) { - auto constexpr Alpha = 0.6L; + constexpr auto 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); + Metropolis_3 strategy(Alpha, 0.0L, 0.0L, 1, 1, false, cdt::RandomSeed{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(); + manifold, move_tracker::MoveType::TWO_SIX, 0.0L)); + auto const after_first = manifold.geometry(); REQUIRE(strategy.attempt_transition( - manifold, move_tracker::move_type::TWO_SIX, 0.0L)); + manifold, move_tracker::MoveType::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); + CHECK_EQ(strategy.geometry().N3_31_13, manifold.N3_31_13()); + CHECK_EQ(strategy.geometry().N3_22, manifold.N3_22()); + CHECK_EQ(strategy.proposed().total(), 2); + CHECK_EQ(strategy.accepted().total(), 2); + CHECK_EQ(strategy.rejected().total(), 0); + CHECK_EQ(strategy.attempted().total(), 2); + CHECK_EQ(strategy.succeeded().total(), 2); + CHECK_EQ(strategy.failed().total(), 0); } } } @@ -546,22 +565,22 @@ SCENARIO("Metropolis transitions are sequential and failure-aware" * { auto manifold = minimal_26_manifold(); auto const before = manifold.delaunay_snapshot(); - Metropolis_3 strategy(Alpha, 0.0L, 0.0L, 1, 1, false, 29); + Metropolis_3 strategy(Alpha, 0.0L, 0.0L, 1, 1, false, cdt::RandomSeed{29}); WHEN("The impossible proposal is attempted.") { auto const accepted = strategy.attempt_transition( - manifold, move_tracker::move_type::SIX_TWO, 0.0L); + manifold, move_tracker::MoveType::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); + CHECK_EQ(strategy.proposed().total(), 1); + CHECK_EQ(strategy.accepted().total(), 0); + CHECK_EQ(strategy.rejected().total(), 1); + CHECK_EQ(strategy.attempted().total(), 1); + CHECK_EQ(strategy.succeeded().total(), 0); + CHECK_EQ(strategy.failed().total(), 1); } } } @@ -569,22 +588,22 @@ SCENARIO("Metropolis transitions are sequential and failure-aware" * SCENARIO("Using the Metropolis algorithm" * doctest::test_suite("metropolis")) { - auto constexpr Alpha = static_cast(0.6); - auto constexpr K = static_cast(1.1); // NOLINT - auto constexpr Lambda = static_cast(0.1); + constexpr auto Alpha = static_cast(0.6); + constexpr auto K = static_cast(1.1); // NOLINT + constexpr auto Lambda = static_cast(0.1); GIVEN("A correctly-constructed Manifold_3.") { - auto constexpr simplices = 640; - auto constexpr timeslices = 4; + constexpr auto simplices = 640; + constexpr auto timeslices = 4; 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, - false, 31); + constexpr auto output_every_n_passes = 1; + constexpr auto passes = 1; + Metropolis_3 testrun(Alpha, K, Lambda, passes, output_every_n_passes, + false, cdt::RandomSeed{31}); THEN("A lot of moves are done.") { auto result = testrun(universe); @@ -592,12 +611,12 @@ SCENARIO("Using the Metropolis algorithm" * doctest::test_suite("metropolis")) CHECK(result.is_valid()); AND_THEN("The correct number of moves are attempted.") { - auto total_proposed = testrun.get_proposed().total(); - auto total_accepted = testrun.get_accepted().total(); - 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(); + auto total_proposed = testrun.proposed().total(); + auto total_accepted = testrun.accepted().total(); + auto total_rejected = testrun.rejected().total(); + auto total_attempted = testrun.attempted().total(); + auto total_successful = testrun.succeeded().total(); + auto total_failed = testrun.failed().total(); CHECK_EQ(total_proposed, universe.N3() * passes); CHECK_EQ(total_proposed, total_accepted + total_rejected); CHECK_EQ(total_attempted, total_proposed); @@ -606,9 +625,9 @@ SCENARIO("Using the Metropolis algorithm" * doctest::test_suite("metropolis")) CHECK_EQ(total_attempted, total_successful + total_failed); 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()); + CHECK_EQ(testrun.geometry().N3, result.N3()); + CHECK_EQ(testrun.geometry().N3_31_13, result.N3_31_13()); + CHECK_EQ(testrun.geometry().N3_22, result.N3_22()); } } } @@ -618,9 +637,9 @@ SCENARIO("Using the Metropolis algorithm" * doctest::test_suite("metropolis")) SCENARIO("Metropolis runs replay every transition from an identical start" * doctest::test_suite("metropolis")) { - auto const initial = minimal_23_manifold(); - auto constexpr seed = cdt::Random_seed{92}; - auto constexpr passes = Int_precision{2}; + auto const initial = minimal_23_manifold(); + constexpr auto seed = cdt::RandomSeed{92}; + constexpr auto passes = Int_precision{2}; CAPTURE(seed); Metropolis_3 first{ 0.6L, @@ -648,14 +667,14 @@ SCENARIO("Metropolis runs replay every transition from an identical start" * }; 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())); - CHECK_EQ(first.transition_count(), first.get_proposed().total()); - CHECK_EQ(replay.transition_count(), replay.get_proposed().total()); + CHECK(same_counts(first.proposed(), replay.proposed())); + CHECK(same_counts(first.accepted(), replay.accepted())); + CHECK(same_counts(first.rejected(), replay.rejected())); + CHECK(same_counts(first.attempted(), replay.attempted())); + CHECK(same_counts(first.succeeded(), replay.succeeded())); + CHECK(same_counts(first.failed(), replay.failed())); + CHECK_EQ(first.transition_count(), first.proposed().total()); + CHECK_EQ(replay.transition_count(), replay.proposed().total()); CHECK_EQ(first.transition_trace(), replay.transition_trace()); } @@ -664,29 +683,29 @@ SCENARIO("Metropolis multi-pass accounting is per invocation" * { GIVEN("A fixed seed and a four-pass Metropolis strategy.") { - auto const initial = minimal_23_manifold(); - auto constexpr passes = Int_precision{4}; - auto constexpr checkpoint = Int_precision{2}; - auto constexpr seed = cdt::Random_seed{103}; - auto constexpr expected = expected_metropolis_fixture(); - Metropolis_3 strategy(0.6L, 0.0L, 0.0L, passes, checkpoint, false, seed); + auto const initial = minimal_23_manifold(); + constexpr auto passes = Int_precision{4}; + constexpr auto checkpoint = Int_precision{2}; + constexpr auto seed = cdt::RandomSeed{103}; + constexpr auto expected = expected_metropolis_fixture(); + Metropolis_3 strategy(0.6L, 0.0L, 0.0L, passes, checkpoint, false, seed); CAPTURE(seed); CAPTURE(expected.standard_library); WHEN("The strategy and a fresh replay each run twice.") { static_cast(strategy(initial)); - auto const first_attempted = strategy.get_attempted().total(); - auto const first_succeeded = strategy.get_succeeded().total(); - auto const first_failed = strategy.get_failed().total(); + auto const first_attempted = strategy.attempted().total(); + auto const first_succeeded = strategy.succeeded().total(); + auto const first_failed = strategy.failed().total(); auto const first_trace = strategy.transition_trace(); auto const first_transitions = strategy.transition_count(); auto const first_checkpoints = strategy.checkpoint_events(); static_cast(strategy(initial)); - auto const second_attempted = strategy.get_attempted().total(); - auto const second_succeeded = strategy.get_succeeded().total(); - auto const second_failed = strategy.get_failed().total(); + auto const second_attempted = strategy.attempted().total(); + auto const second_succeeded = strategy.succeeded().total(); + auto const second_failed = strategy.failed().total(); auto const second_trace = strategy.transition_trace(); auto const second_transitions = strategy.transition_count(); auto const second_checkpoints = strategy.checkpoint_events(); @@ -723,9 +742,9 @@ SCENARIO("Metropolis multi-pass accounting is per invocation" * SCENARIO("Metropolis provenance is derived from the actual run" * doctest::test_suite("metropolis")) { - GIVEN("A default-constructed strategy") + GIVEN("A strategy constructed without an explicit random stream") { - Metropolis_3 strategy; + Metropolis_3 strategy{0.6L, 1.1L, 0.1L, 1, 1, false}; THEN("Its run-owned generator uses the named transition stream.") { CHECK_EQ(strategy.stream(), cdt::random_streams::transitions); } @@ -734,16 +753,17 @@ SCENARIO("Metropolis provenance is derived from the actual run" * GIVEN("An injected generator and stale caller-supplied run metadata") { auto const manifold = minimal_23_manifold(); - utilities::Reproducibility_metadata supplied{.seed = 7, - .transition_stream = 99, - .alpha = 0.7L, - .k = 2.0L, - .lambda = 3.0L, - .configured_passes = 100, - .checkpoint_interval = 50}; - auto constexpr seed = cdt::Random_seed{92}; - auto constexpr transition_stream = cdt::Random_stream{17}; - Metropolis_3 strategy{ + utilities::Reproducibility_metadata supplied{ + .seed = cdt::RandomSeed{7}, + .transition_stream = cdt::RandomStream{99}, + .alpha = 0.7L, + .k = 2.0L, + .lambda = 3.0L, + .configured_passes = 100, + .checkpoint_interval = 50}; + constexpr auto seed = cdt::RandomSeed{92}; + constexpr auto transition_stream = cdt::RandomStream{17}; + Metropolis_3 strategy{ 0.6L, 1.1L, 0.1L, 2, 1, false, cdt::Random{seed, transition_stream}, supplied }; @@ -751,7 +771,7 @@ SCENARIO("Metropolis provenance is derived from the actual run" * WHEN("Output provenance is materialized") { auto const metadata = strategy.reproducibility_metadata( - manifold, utilities::Artifact_kind::FINAL_TRIANGULATION, 0); + manifold, utilities::ArtifactKind::FINAL_TRIANGULATION, 0); THEN("Run-owned seed, stream, action, and cadence replace stale claims.") { diff --git a/tests/Move_always_test.cpp b/tests/Move_always_test.cpp index 396b2221c..c177c263d 100644 --- a/tests/Move_always_test.cpp +++ b/tests/Move_always_test.cpp @@ -24,9 +24,9 @@ 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{ + constexpr auto radius = 2.0 * std::numbers::inv_sqrt3_v; + constexpr auto 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}, @@ -48,7 +48,7 @@ namespace { Expected_run_accounting first; Expected_run_accounting second; - move_tracker::move_type continuation_move; + move_tracker::MoveType continuation_move; Int_precision first_continuation_attempts; Int_precision second_continuation_attempts; char const* standard_library; @@ -62,7 +62,7 @@ namespace return { .first = {10, 1, 9}, .second = { 9, 2, 7}, - .continuation_move = move_tracker::move_type::TWO_THREE, + .continuation_move = move_tracker::MoveType::TWO_THREE, .first_continuation_attempts = 3, .second_continuation_attempts = 1, .standard_library = "libc++" @@ -71,7 +71,7 @@ namespace return { .first = {9, 3, 6}, .second = {9, 3, 6}, - .continuation_move = move_tracker::move_type::TWO_SIX, + .continuation_move = move_tracker::MoveType::TWO_SIX, .first_continuation_attempts = 3, .second_continuation_attempts = 1, .standard_library = "libstdc++" @@ -80,7 +80,7 @@ namespace return { .first = {8, 0, 8}, .second = {9, 2, 7}, - .continuation_move = move_tracker::move_type::TWO_THREE, + .continuation_move = move_tracker::MoveType::TWO_THREE, .first_continuation_attempts = 0, .second_continuation_attempts = 1, .standard_library = "msvc-stl" @@ -151,38 +151,38 @@ SCENARIO("MoveAlways member functions" * doctest::test_suite("move_always")) spdlog::debug("MoveAlways member functions.\n"); GIVEN("A correctly-constructed Manifold_3.") { - auto constexpr simplices = 640; - auto constexpr timeslices = 4; + constexpr auto simplices = 640; + constexpr auto timeslices = 4; 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, cdt::Random_seed{92}); + constexpr auto passes = 10; + constexpr auto checkpoint = 5; + MoveAlways_3 const mover(passes, checkpoint, cdt::RandomSeed{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, cdt::Random_seed{92}), + CHECK_THROWS_AS(MoveAlways_3(-1, checkpoint, cdt::RandomSeed{92}), std::invalid_argument); - CHECK_THROWS_AS(MoveAlways_3(0, checkpoint, cdt::Random_seed{92}), + CHECK_THROWS_AS(MoveAlways_3(0, checkpoint, cdt::RandomSeed{92}), std::invalid_argument); - CHECK_THROWS_AS(MoveAlways_3(passes, 0, cdt::Random_seed{92}), + CHECK_THROWS_AS(MoveAlways_3(passes, 0, cdt::RandomSeed{92}), std::invalid_argument); THEN("Attempted, successful, and failed moves are zero-initialized.") { - CHECK_EQ(mover.get_attempted().total(), 0); - CHECK_EQ(mover.get_succeeded().total(), 0); - CHECK_EQ(mover.get_failed().total(), 0); + CHECK_EQ(mover.attempted().total(), 0); + CHECK_EQ(mover.succeeded().total(), 0); + CHECK_EQ(mover.failed().total(), 0); } } WHEN("A MoveAlways_3 algorithm is instantiated.") { - auto constexpr passes = 1; - auto constexpr checkpoint = 1; - MoveAlways_3 const mover(passes, checkpoint, cdt::Random_seed{92}); + constexpr auto passes = 1; + constexpr auto checkpoint = 1; + MoveAlways_3 const mover(passes, checkpoint, cdt::RandomSeed{92}); THEN("The correct passes and checkpoints are instantiated.") { CHECK_EQ(mover.passes(), passes); @@ -190,9 +190,9 @@ SCENARIO("MoveAlways member functions" * doctest::test_suite("move_always")) } THEN("Attempted moves and successful moves are zero-initialized.") { - CHECK_EQ(mover.get_attempted().total(), 0); - CHECK_EQ(mover.get_succeeded().total(), 0); - CHECK_EQ(mover.get_failed().total(), 0); + CHECK_EQ(mover.attempted().total(), 0); + CHECK_EQ(mover.succeeded().total(), 0); + CHECK_EQ(mover.failed().total(), 0); } } } @@ -203,42 +203,42 @@ SCENARIO("MoveAlways multi-pass accounting is per invocation" * { GIVEN("A fixed seed and a four-pass MoveAlways strategy.") { - auto const initial = minimal_23_manifold(); - auto constexpr passes = Int_precision{4}; - auto constexpr checkpoint = Int_precision{2}; - auto constexpr seed = cdt::Random_seed{103}; - auto constexpr expected = expected_move_always_fixture(); - MoveAlways_3 strategy(passes, checkpoint, seed, false); + auto const initial = minimal_23_manifold(); + constexpr auto passes = Int_precision{4}; + constexpr auto checkpoint = Int_precision{2}; + constexpr auto seed = cdt::RandomSeed{103}; + constexpr auto expected = expected_move_always_fixture(); + MoveAlways_3 strategy(passes, checkpoint, seed, false); CAPTURE(seed); CAPTURE(expected.standard_library); WHEN("The strategy and a fresh replay each run twice.") { auto const first_result = strategy(initial); - auto const first_attempted_moves = strategy.get_attempted(); + auto const first_attempted_moves = strategy.attempted(); auto const first_attempted = first_attempted_moves.total(); - auto const first_succeeded = strategy.get_succeeded().total(); - auto const first_failed = strategy.get_failed().total(); + auto const first_succeeded = strategy.succeeded().total(); + auto const first_failed = strategy.failed().total(); auto const first_checkpoints = strategy.checkpoint_events(); auto const second_result = strategy(initial); - auto const second_attempted_moves = strategy.get_attempted(); + auto const second_attempted_moves = strategy.attempted(); auto const second_attempted = second_attempted_moves.total(); - auto const second_succeeded = strategy.get_succeeded().total(); - auto const second_failed = strategy.get_failed().total(); + auto const second_succeeded = strategy.succeeded().total(); + auto const second_failed = strategy.failed().total(); auto const second_checkpoints = strategy.checkpoint_events(); MoveAlways_3 replay(passes, checkpoint, seed, false); auto const replay_first_result = replay(initial); - auto const replay_first_attempted = replay.get_attempted().total(); - auto const replay_first_succeeded = replay.get_succeeded().total(); - auto const replay_first_failed = replay.get_failed().total(); + auto const replay_first_attempted = replay.attempted().total(); + auto const replay_first_succeeded = replay.succeeded().total(); + auto const replay_first_failed = replay.failed().total(); auto const replay_first_checkpoints = replay.checkpoint_events(); auto const replay_second_result = replay(initial); - auto const replay_second_attempted = replay.get_attempted().total(); - auto const replay_second_succeeded = replay.get_succeeded().total(); - auto const replay_second_failed = replay.get_failed().total(); + auto const replay_second_attempted = replay.attempted().total(); + auto const replay_second_succeeded = replay.succeeded().total(); + auto const replay_second_failed = replay.failed().total(); auto const replay_second_checkpoints = replay.checkpoint_events(); THEN("Each invocation has exact accounting and is replayable.") @@ -282,15 +282,15 @@ 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; + constexpr auto simplices = 64; + constexpr auto timeslices = 3; 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 = 2; - MoveAlways_3 mover(passes, checkpoint, cdt::Random_seed{92}); + constexpr auto passes = 1; + constexpr auto checkpoint = 2; + MoveAlways_3 mover(passes, checkpoint, cdt::RandomSeed{92}); THEN("A lot of moves are made.") { auto result = mover(manifold); @@ -300,9 +300,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()); + CHECK_EQ(mover.attempted().total(), manifold.N3()); + CHECK_EQ(mover.attempted().total(), + mover.succeeded().total() + mover.failed().total()); } } } diff --git a/tests/Move_command_test.cpp b/tests/Move_command_test.cpp index 45c890b06..4ab60f7ad 100644 --- a/tests/Move_command_test.cpp +++ b/tests/Move_command_test.cpp @@ -45,10 +45,10 @@ namespace Int_precision n0; }; - [[nodiscard]] auto constexpr expected_geometry_delta( - move_tracker::move_type const move) -> ExpectedGeometryDelta + [[nodiscard]] constexpr auto expected_geometry_delta( + move_tracker::MoveType const move) -> ExpectedGeometryDelta { - using enum move_tracker::move_type; + using enum move_tracker::MoveType; switch (move) { case TWO_THREE: return {1, 0, 0, 0, 1, 2, 1, 1, 0, 0}; @@ -60,9 +60,9 @@ namespace return {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; } - void check_successful_move_outcome(Manifold_3 const& before, - Manifold_3 const& after, - move_tracker::move_type const move) + void check_successful_move_outcome(Manifold_3 const& before, + Manifold_3 const& after, + move_tracker::MoveType const move) { auto const expected = expected_geometry_delta(move); CHECK(after.is_correct()); @@ -148,22 +148,22 @@ namespace } static_assert( - std::is_same_v const&>() - .get_succeeded()), - move_tracker::MoveTracker const&>); + std::is_same_v< + decltype(std::declval const&>().succeeded()), + move_tracker::MoveTracker const&>); void check_single_move_outcome(MoveCommand const& command, Manifold_3 const& before, - move_tracker::move_type const move, + move_tracker::MoveType const move, Int_precision const cell_delta) { - auto const attempted = command.get_attempted()[move]; - auto const succeeded = command.get_succeeded()[move]; - auto const failed = command.get_failed()[move]; + auto const attempted = command.attempted()[move]; + auto const succeeded = command.succeeded()[move]; + auto const failed = command.failed()[move]; CHECK_EQ(attempted, 1); CHECK_EQ(succeeded + failed, 1); - auto const& result = command.get_const_results(); + auto const& result = command.result(); CHECK(result.is_valid()); if (succeeded == 1) { @@ -234,8 +234,8 @@ SCENARIO("Invoking a move with a function pointer" * spdlog::debug("Invoking a move with a function pointer.\n"); GIVEN("A valid manifold.") { - auto constexpr desired_simplices = 640; - auto constexpr desired_timeslices = 4; + constexpr auto desired_simplices = 640; + constexpr auto desired_timeslices = 4; Manifold_3 manifold(desired_simplices, desired_timeslices, cdt::Random{92}); REQUIRE(manifold.is_correct()); WHEN("A function pointer is constructed for a move.") @@ -248,7 +248,7 @@ SCENARIO("Invoking a move with a function pointer" * auto result = move23(manifold, random); REQUIRE(result.has_value()); check_successful_move_outcome(manifold, *result, - move_tracker::move_type::TWO_THREE); + move_tracker::MoveType::TWO_THREE); } } } @@ -259,8 +259,8 @@ SCENARIO("Invoking a move with a lambda" * doctest::test_suite("move_command")) spdlog::debug("Invoking a move with a lambda.\n"); GIVEN("A valid manifold.") { - auto constexpr desired_simplices = 640; - auto constexpr desired_timeslices = 4; + constexpr auto desired_simplices = 640; + constexpr auto desired_timeslices = 4; Manifold_3 manifold(desired_simplices, desired_timeslices, cdt::Random{92}); REQUIRE(manifold.is_correct()); WHEN("A lambda is constructed for a move.") @@ -275,7 +275,7 @@ SCENARIO("Invoking a move with a lambda" * doctest::test_suite("move_command")) CAPTURE(random.seed()); auto result = move23(manifold, random); check_successful_move_outcome(manifold, result, - move_tracker::move_type::TWO_THREE); + move_tracker::MoveType::TWO_THREE); } } } @@ -287,8 +287,8 @@ SCENARIO("Invoking a move with apply_move and a function pointer" * spdlog::debug("Invoking a move with apply_move and a function pointer.\n"); GIVEN("A valid manifold.") { - auto constexpr desired_simplices = 640; - auto constexpr desired_timeslices = 4; + constexpr auto desired_simplices = 640; + constexpr auto desired_timeslices = 4; Manifold_3 manifold(desired_simplices, desired_timeslices, cdt::Random{92}); REQUIRE(manifold.is_correct()); WHEN("Apply_move is used for a move.") @@ -301,7 +301,7 @@ SCENARIO("Invoking a move with apply_move and a function pointer" * auto result = apply_move(manifold, move, random); REQUIRE(result.has_value()); check_successful_move_outcome(manifold, *result, - move_tracker::move_type::TWO_THREE); + move_tracker::MoveType::TWO_THREE); } } } @@ -312,8 +312,8 @@ SCENARIO("MoveCommand initialization" * doctest::test_suite("move_command")) spdlog::debug("MoveCommand initialization.\n"); GIVEN("A valid manifold.") { - auto constexpr desired_simplices = 640; - auto constexpr desired_timeslices = 4; + constexpr auto desired_simplices = 640; + constexpr auto desired_timeslices = 4; Manifold_3 manifold(desired_simplices, desired_timeslices, cdt::Random{92}); REQUIRE(manifold.is_correct()); WHEN("A Command is constructed with a manifold.") @@ -322,30 +322,30 @@ SCENARIO("MoveCommand initialization" * doctest::test_suite("move_command")) THEN("The original is still valid.") { REQUIRE(manifold.is_correct()); } THEN("It contains the manifold.") { - CHECK_EQ(manifold.N3(), command.get_const_results().N3()); - CHECK_EQ(manifold.N3_31(), command.get_const_results().N3_31()); - CHECK_EQ(manifold.N3_22(), command.get_const_results().N3_22()); - CHECK_EQ(manifold.N3_13(), command.get_const_results().N3_13()); - CHECK_EQ(manifold.N3_31_13(), command.get_const_results().N3_31_13()); - CHECK_EQ(manifold.N2(), command.get_const_results().N2()); - CHECK_EQ(manifold.N1(), command.get_const_results().N1()); - CHECK_EQ(manifold.N1_TL(), command.get_const_results().N1_TL()); - CHECK_EQ(manifold.N1_SL(), command.get_const_results().N1_SL()); - 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()); + CHECK_EQ(manifold.N3(), command.result().N3()); + CHECK_EQ(manifold.N3_31(), command.result().N3_31()); + CHECK_EQ(manifold.N3_22(), command.result().N3_22()); + CHECK_EQ(manifold.N3_13(), command.result().N3_13()); + CHECK_EQ(manifold.N3_31_13(), command.result().N3_31_13()); + CHECK_EQ(manifold.N2(), command.result().N2()); + CHECK_EQ(manifold.N1(), command.result().N1()); + CHECK_EQ(manifold.N1_TL(), command.result().N1_TL()); + CHECK_EQ(manifold.N1_SL(), command.result().N1_SL()); + CHECK_EQ(manifold.N0(), command.result().N0()); + CHECK_EQ(manifold.max_time(), command.result().max_time()); + CHECK_EQ(manifold.min_time(), command.result().min_time()); } THEN("The two manifolds are distinct.") { auto* manifold_ptr = &manifold; - auto const* manifold2_ptr = &command.get_const_results(); + auto const* manifold2_ptr = &command.result(); CHECK_NE(manifold_ptr, manifold2_ptr); } THEN("Attempted, succeeded, and failed moves are initialized to 0.") { - CHECK_EQ(command.get_attempted().total(), 0); - CHECK_EQ(command.get_succeeded().total(), 0); - CHECK_EQ(command.get_failed().total(), 0); + CHECK_EQ(command.attempted().total(), 0); + CHECK_EQ(command.succeeded().total(), 0); + CHECK_EQ(command.failed().total(), 0); } } } @@ -356,8 +356,8 @@ SCENARIO("Queueing and executing moves" * doctest::test_suite("move_command")) spdlog::debug("Queueing and executing moves.\n"); GIVEN("A valid manifold.") { - auto constexpr desired_simplices = 9600; - auto constexpr desired_timeslices = 7; + constexpr auto desired_simplices = 9600; + constexpr auto desired_timeslices = 7; Manifold_3 manifold(desired_simplices, desired_timeslices, cdt::Random{92}); REQUIRE(manifold.is_correct()); WHEN("Move_command copies the manifold and applies the move.") @@ -368,17 +368,17 @@ SCENARIO("Queueing and executing moves" * doctest::test_suite("move_command")) MoveCommand command(manifold); // Note: If we do a move that expands the size of the manifold, // without the copy ctor this will Segfault! - command.enqueue(move_tracker::move_type::THREE_TWO); + command.enqueue(move_tracker::MoveType::THREE_TWO); // Execute the move cdt::Random random{92}; CAPTURE(random.seed()); command.execute(random); check_single_move_outcome(command, manifold, - move_tracker::move_type::THREE_TWO, -1); + move_tracker::MoveType::THREE_TWO, -1); // Get the results - auto result = command.get_results(); + auto result = command.result(); // Distinct objects? auto* manifold_ptr = &manifold; @@ -391,7 +391,7 @@ SCENARIO("Queueing and executing moves" * doctest::test_suite("move_command")) WHEN("A (4,4) move is queued.") { MoveCommand command(manifold); - command.enqueue(move_tracker::move_type::FOUR_FOUR); + command.enqueue(move_tracker::MoveType::FOUR_FOUR); THEN("It is executed correctly.") { // Execute the move @@ -399,13 +399,13 @@ SCENARIO("Queueing and executing moves" * doctest::test_suite("move_command")) CAPTURE(random.seed()); command.execute(random); check_single_move_outcome(command, manifold, - move_tracker::move_type::FOUR_FOUR, 0); + move_tracker::MoveType::FOUR_FOUR, 0); } } WHEN("A (2,3) move is queued.") { MoveCommand command(manifold); - command.enqueue(move_tracker::move_type::TWO_THREE); + command.enqueue(move_tracker::MoveType::TWO_THREE); THEN("It is executed correctly.") { // Execute the move @@ -413,13 +413,13 @@ SCENARIO("Queueing and executing moves" * doctest::test_suite("move_command")) CAPTURE(random.seed()); command.execute(random); check_single_move_outcome(command, manifold, - move_tracker::move_type::TWO_THREE, 1); + move_tracker::MoveType::TWO_THREE, 1); } } WHEN("A (3,2) move is queued.") { MoveCommand command(manifold); - command.enqueue(move_tracker::move_type::THREE_TWO); + command.enqueue(move_tracker::MoveType::THREE_TWO); THEN("It is executed correctly.") { // Execute the move @@ -427,13 +427,13 @@ SCENARIO("Queueing and executing moves" * doctest::test_suite("move_command")) CAPTURE(random.seed()); command.execute(random); check_single_move_outcome(command, manifold, - move_tracker::move_type::THREE_TWO, -1); + move_tracker::MoveType::THREE_TWO, -1); } } WHEN("A (2,6) move is queued.") { MoveCommand command(manifold); - command.enqueue(move_tracker::move_type::TWO_SIX); + command.enqueue(move_tracker::MoveType::TWO_SIX); THEN("It is executed correctly.") { // Execute the move @@ -441,13 +441,13 @@ SCENARIO("Queueing and executing moves" * doctest::test_suite("move_command")) CAPTURE(random.seed()); command.execute(random); check_single_move_outcome(command, manifold, - move_tracker::move_type::TWO_SIX, 4); + move_tracker::MoveType::TWO_SIX, 4); } } WHEN("A (6,2) move is queued.") { MoveCommand command(manifold); - command.enqueue(move_tracker::move_type::SIX_TWO); + command.enqueue(move_tracker::MoveType::SIX_TWO); THEN("It is executed correctly.") { // Execute the move @@ -455,7 +455,7 @@ SCENARIO("Queueing and executing moves" * doctest::test_suite("move_command")) CAPTURE(random.seed()); command.execute(random); check_single_move_outcome(command, manifold, - move_tracker::move_type::SIX_TWO, -4); + move_tracker::MoveType::SIX_TWO, -4); } } } @@ -466,7 +466,7 @@ SCENARIO("Rejected moves preserve manifold state" * { GIVEN("A single tetrahedron with no movable (2,2) simplex.") { - auto constexpr radius_2 = 2.0 * std::numbers::inv_sqrt3_v; + constexpr auto radius_2 = 2.0 * std::numbers::inv_sqrt3_v; Causal_vertices_t<3> causal_vertices; causal_vertices.emplace_back(Point_t<3>{1, 0, 0}, 1); causal_vertices.emplace_back(Point_t<3>{0, 1, 0}, 1); @@ -476,7 +476,7 @@ SCENARIO("Rejected moves preserve manifold state" * REQUIRE(manifold.is_correct()); REQUIRE_EQ(manifold.N3_22(), 0); MoveCommand command(manifold); - command.enqueue(move_tracker::move_type::TWO_THREE); + command.enqueue(move_tracker::MoveType::TWO_THREE); WHEN("The move is executed.") { @@ -485,9 +485,9 @@ SCENARIO("Rejected moves preserve manifold state" * command.execute(random); THEN("The rejection leaves the complete manifold unchanged.") { - REQUIRE_EQ(command.get_failed().two_three_moves(), 1); + REQUIRE_EQ(command.failed().two_three_moves(), 1); check_single_move_outcome(command, manifold, - move_tracker::move_type::TWO_THREE, 1); + move_tracker::MoveType::TWO_THREE, 1); } } } @@ -499,16 +499,16 @@ SCENARIO("Executing multiple moves on the queue" * spdlog::debug("Executing multiple moves on the queue.\n"); GIVEN("A valid manifold") { - auto constexpr desired_simplices = 9600; - auto constexpr desired_timeslices = 7; + constexpr auto desired_simplices = 9600; + constexpr auto desired_timeslices = 7; Manifold_3 const manifold(desired_simplices, desired_timeslices, cdt::Random{92}); REQUIRE(manifold.is_correct()); WHEN("(2,3) and (3,2) moves are queued.") { MoveCommand command(manifold); - command.enqueue(move_tracker::move_type::TWO_THREE); - command.enqueue(move_tracker::move_type::THREE_TWO); + command.enqueue(move_tracker::MoveType::TWO_THREE); + command.enqueue(move_tracker::MoveType::THREE_TWO); THEN("There are two moves in the queue.") { CHECK_EQ(command.size(), 2); } THEN("The moves are executed correctly.") { @@ -518,16 +518,15 @@ SCENARIO("Executing multiple moves on the queue" * command.execute(random); // There should be 2 attempted moves - CHECK_EQ(command.get_attempted().total(), 2); + CHECK_EQ(command.attempted().total(), 2); - auto successful_23_moves = command.get_succeeded().two_three_moves(); - auto successful_32_moves = command.get_succeeded().three_two_moves(); + auto successful_23_moves = command.succeeded().two_three_moves(); + auto successful_32_moves = command.succeeded().three_two_moves(); - CHECK_EQ(command.get_succeeded().total() + command.get_failed().total(), - 2); + CHECK_EQ(command.succeeded().total() + command.failed().total(), 2); // Get the results - auto const& result = command.get_const_results(); + auto const& result = command.result(); CHECK_EQ( result.simplices(), @@ -538,11 +537,11 @@ SCENARIO("Executing multiple moves on the queue" * WHEN("One of each move is queued.") { MoveCommand command(manifold); - command.enqueue(move_tracker::move_type::TWO_THREE); - command.enqueue(move_tracker::move_type::TWO_SIX); - command.enqueue(move_tracker::move_type::FOUR_FOUR); - command.enqueue(move_tracker::move_type::SIX_TWO); - command.enqueue(move_tracker::move_type::THREE_TWO); + command.enqueue(move_tracker::MoveType::TWO_THREE); + command.enqueue(move_tracker::MoveType::TWO_SIX); + command.enqueue(move_tracker::MoveType::FOUR_FOUR); + command.enqueue(move_tracker::MoveType::SIX_TWO); + command.enqueue(move_tracker::MoveType::THREE_TWO); THEN("There are five moves in the queue.") { CHECK_EQ(command.size(), 5); } THEN("The moves are executed correctly.") @@ -553,18 +552,17 @@ SCENARIO("Executing multiple moves on the queue" * command.execute(random); // There should be 5 attempted moves - CHECK_EQ(command.get_attempted().total(), 5); + CHECK_EQ(command.attempted().total(), 5); - auto successful_23_moves = command.get_succeeded().two_three_moves(); - auto successful_26_moves = command.get_succeeded().two_six_moves(); - auto successful_62_moves = command.get_succeeded().six_two_moves(); - auto successful_32_moves = command.get_succeeded().three_two_moves(); + auto successful_23_moves = command.succeeded().two_three_moves(); + auto successful_26_moves = command.succeeded().two_six_moves(); + auto successful_62_moves = command.succeeded().six_two_moves(); + auto successful_32_moves = command.succeeded().three_two_moves(); - CHECK_EQ(command.get_succeeded().total() + command.get_failed().total(), - 5); + CHECK_EQ(command.succeeded().total() + command.failed().total(), 5); // Get the results - auto const& result = command.get_const_results(); + auto const& result = command.result(); CHECK_EQ(result.simplices(), manifold.simplices() + successful_23_moves - diff --git a/tests/Move_outcome_test.cpp b/tests/Move_outcome_test.cpp index 606ddfb82..b42c73cf6 100644 --- a/tests/Move_outcome_test.cpp +++ b/tests/Move_outcome_test.cpp @@ -25,55 +25,55 @@ namespace using cdt::ergodic_moves::MoveError; using cdt::ergodic_moves::MoveFailure; using cdt::ergodic_moves::MoveOutcome; - using cdt::move_tracker::move_type; + using cdt::move_tracker::MoveType; struct Failure_case { MoveFailure failure; - move_type move; + MoveType move; MoveOutcome outcome; std::string_view message; }; - auto constexpr failure_cases = std::array{ + constexpr auto failure_cases = std::array{ Failure_case{.failure = MoveFailure::NO_CANDIDATE, - .move = move_type::TWO_THREE, + .move = MoveType::TWO_THREE, .outcome = MoveOutcome::INAPPLICABLE, .message = "No raw proposal site is available." }, Failure_case{ .failure = MoveFailure::INVALID_TOPOLOGY, - .move = move_type::THREE_TWO, + .move = MoveType::THREE_TWO, .outcome = MoveOutcome::INAPPLICABLE, .message = "The selected proposal site is not part of the triangulation." }, Failure_case{ .failure = MoveFailure::CAUSAL_INVALIDITY, - .move = move_type::TWO_SIX, + .move = MoveType::TWO_SIX, .outcome = MoveOutcome::INAPPLICABLE, .message = "The selected proposal site violates a CDT move invariant." }, Failure_case{ .failure = MoveFailure::STALE_CANDIDATE, - .move = move_type::SIX_TWO, + .move = MoveType::SIX_TWO, .outcome = MoveOutcome::EXECUTION_FAILED, .message = "The prepared proposal site no longer exists." }, Failure_case{ .failure = MoveFailure::EXECUTION_FAILURE, - .move = move_type::FOUR_FOUR, + .move = MoveType::FOUR_FOUR, .outcome = MoveOutcome::EXECUTION_FAILED, .message = "CGAL rejected execution of the prepared move." }, Failure_case{ .failure = MoveFailure::INVARIANT_VIOLATION, - .move = move_type::TWO_THREE, + .move = MoveType::TWO_THREE, .outcome = MoveOutcome::EXECUTION_FAILED, .message = "The executed move violated a manifold postcondition."}, Failure_case{ .failure = MoveFailure::UNKNOWN_MOVE, - .move = move_type::FOUR_FOUR, + .move = MoveType::FOUR_FOUR, .outcome = MoveOutcome::EXECUTION_FAILED, .message = "The requested Pachner move is unknown." } }; - auto constexpr sample_error = + constexpr auto sample_error = MoveError{.category = MoveFailure::STALE_CANDIDATE, - .requested_move = move_type::SIX_TWO}; + .requested_move = MoveType::SIX_TWO}; } // namespace static_assert(std::same_as, @@ -83,7 +83,7 @@ static_assert(std::is_trivially_destructible_v); static_assert(std::is_nothrow_copy_constructible_v); static_assert(std::is_nothrow_copy_assignable_v); static_assert(sample_error.reason() == MoveFailure::STALE_CANDIDATE); -static_assert(sample_error.move() == move_type::SIX_TWO); +static_assert(sample_error.move() == MoveType::SIX_TWO); static_assert(sample_error.message() == "The prepared proposal site no longer exists."); static_assert(cdt::ergodic_moves::outcome_from(sample_error) == @@ -123,34 +123,53 @@ TEST_CASE("Move failures have stable diagnostics and accounting outcomes" * } } -TEST_CASE("Move error equality includes failure and move identity" * - doctest::test_suite("move_outcome")) +SCENARIO("Move error equality includes failure and move identity" * + doctest::test_suite("move_outcome")) { - auto const reference = MoveError{.category = MoveFailure::NO_CANDIDATE, - .requested_move = move_type::TWO_THREE}; - auto const same = MoveError{.category = MoveFailure::NO_CANDIDATE, - .requested_move = move_type::TWO_THREE}; - auto const different_failure = - MoveError{.category = MoveFailure::INVALID_TOPOLOGY, - .requested_move = move_type::TWO_THREE}; - auto const different_move = MoveError{.category = MoveFailure::NO_CANDIDATE, - .requested_move = move_type::THREE_TWO}; - - CHECK_EQ(reference, same); - CHECK_NE(reference, different_failure); - CHECK_NE(reference, different_move); + GIVEN("A move error and variants that change one identity field") + { + auto const reference = MoveError{.category = MoveFailure::NO_CANDIDATE, + .requested_move = MoveType::TWO_THREE}; + auto const same = MoveError{.category = MoveFailure::NO_CANDIDATE, + .requested_move = MoveType::TWO_THREE}; + auto const different_failure = + MoveError{.category = MoveFailure::INVALID_TOPOLOGY, + .requested_move = MoveType::TWO_THREE}; + auto const different_move = + MoveError{.category = MoveFailure::NO_CANDIDATE, + .requested_move = MoveType::THREE_TWO}; + + WHEN("The errors are compared") + { + THEN("Both the failure category and requested move determine equality") + { + CHECK_EQ(reference, same); + CHECK_NE(reference, different_failure); + CHECK_NE(reference, different_move); + } + } + } } -TEST_CASE("Out-of-range move failures fail closed" * - doctest::test_suite("move_outcome")) +SCENARIO("Out-of-range move failures fail closed" * + doctest::test_suite("move_outcome")) { - auto const error = MoveError{.category = static_cast( - std::numeric_limits::max()), - .requested_move = move_type::TWO_SIX}; - - CHECK_EQ(error.move(), move_type::TWO_SIX); - CHECK_EQ(error.message(), "The move failed for an unknown reason."); - CHECK_EQ(cdt::ergodic_moves::format_as(error), error.message()); - CHECK_EQ(cdt::ergodic_moves::outcome_from(error), - MoveOutcome::EXECUTION_FAILED); + GIVEN("A move error containing an unknown failure category") + { + auto const error = MoveError{.category = static_cast( + std::numeric_limits::max()), + .requested_move = MoveType::TWO_SIX}; + + WHEN("Its public diagnostics and accounting outcome are requested") + { + THEN("The move identity is retained and the failure remains closed") + { + CHECK_EQ(error.move(), MoveType::TWO_SIX); + CHECK_EQ(error.message(), "The move failed for an unknown reason."); + CHECK_EQ(cdt::ergodic_moves::format_as(error), error.message()); + CHECK_EQ(cdt::ergodic_moves::outcome_from(error), + MoveOutcome::EXECUTION_FAILED); + } + } + } } diff --git a/tests/Move_run_test.cpp b/tests/Move_run_test.cpp index a8f8ed3a6..3696e46f1 100644 --- a/tests/Move_run_test.cpp +++ b/tests/Move_run_test.cpp @@ -21,11 +21,11 @@ namespace { struct ScriptedManifold { - static auto constexpr dimension = 3; + static constexpr auto dimension = 3; - Int_precision simplices{2}; + Int_precision simplices{2}; - [[nodiscard]] auto N3() const noexcept { return simplices; } + [[nodiscard]] auto N3() const noexcept { return simplices; } }; class ScriptedCommand @@ -36,13 +36,13 @@ namespace public: [[nodiscard]] auto results() noexcept -> auto& { return m_results; } - [[nodiscard]] auto get_attempted() const -> auto const& + [[nodiscard]] auto attempted() const -> auto const& { return m_results.attempted; } - [[nodiscard]] auto get_succeeded() const -> auto const& + [[nodiscard]] auto succeeded() const -> auto const& { return m_results.succeeded; } - [[nodiscard]] auto get_failed() const -> auto const& + [[nodiscard]] auto failed() const -> auto const& { return m_results.failed; } void reset_counters() @@ -130,7 +130,7 @@ SCENARIO("MoveCommand results are consumed and reset once" * GIVEN("A command with cumulative attempted, succeeded, and failed counts.") { ScriptedCommand command; - auto constexpr move = move_tracker::move_type::TWO_THREE; + constexpr auto move = move_tracker::MoveType::TWO_THREE; command.results().attempted[move] = 3; command.results().succeeded[move] = 1; command.results().failed[move] = 2; @@ -145,9 +145,9 @@ SCENARIO("MoveCommand results are consumed and reset once" * CHECK_EQ(consumed.attempted.total(), 3); CHECK_EQ(consumed.succeeded.total(), 1); CHECK_EQ(consumed.failed.total(), 2); - CHECK_EQ(command.get_attempted().total(), 0); - CHECK_EQ(command.get_succeeded().total(), 0); - CHECK_EQ(command.get_failed().total(), 0); + CHECK_EQ(command.attempted().total(), 0); + CHECK_EQ(command.succeeded().total(), 0); + CHECK_EQ(command.failed().total(), 0); CHECK_EQ(command.reset_count(), 1); } } @@ -170,14 +170,14 @@ SCENARIO("Shared move-run orchestration accumulates pass deltas once" * auto result = detail::execute_move_run( ScriptedManifold{}, 0, *cadence, detail::MoveRunIdentity{.algorithm = "Scripted", - .seed = Random_seed{103}, - .stream = Random_stream{7}}, + .seed = RandomSeed{103}, + .stream = RandomStream{7}}, true, [](ScriptedManifold current, int pass_index, Int_precision const attempts) { ++pass_index; detail::MoveCommandResults delta; - auto constexpr move = move_tracker::move_type::TWO_THREE; + constexpr auto move = move_tracker::MoveType::TWO_THREE; delta.attempted[move] = attempts; delta.succeeded[move] = pass_index; delta.failed[move] = attempts - pass_index; @@ -232,8 +232,8 @@ SCENARIO( auto result = detail::execute_move_run( ScriptedManifold{}, 0, *cadence, detail::MoveRunIdentity{.algorithm = "Scripted", - .seed = Random_seed{103}, - .stream = Random_stream{7}}, + .seed = RandomSeed{103}, + .stream = RandomStream{7}}, false, [](ScriptedManifold current, int pass_index, Int_precision) { return detail::MovePassResult{ diff --git a/tests/Move_tracker_test.cpp b/tests/Move_tracker_test.cpp index a3d865436..bad9f99e4 100644 --- a/tests/Move_tracker_test.cpp +++ b/tests/Move_tracker_test.cpp @@ -12,6 +12,8 @@ #include +#include +#include #include #include "Manifold.hpp" @@ -21,7 +23,14 @@ using namespace std; using namespace manifolds; using namespace move_tracker; -static_assert(std::is_nothrow_swappable_v>); +static_assert(std::is_nothrow_swappable_v); + +template +concept EnumIntegerConvertible = + requires(Value value) { move_tracker::as_integer(value); }; + +static_assert(EnumIntegerConvertible); +static_assert(!EnumIntegerConvertible); SCENARIO("MoveTracker special members" * doctest::test_suite("move_tracker")) { @@ -32,38 +41,38 @@ SCENARIO("MoveTracker special members" * doctest::test_suite("move_tracker")) { THEN("It is no-throw destructible.") { - REQUIRE(is_nothrow_destructible_v>); + REQUIRE(is_nothrow_destructible_v); spdlog::debug("It is no-throw destructible.\n"); } THEN("It is no-throw default constructible.") { - REQUIRE(is_nothrow_default_constructible_v>); + REQUIRE(is_nothrow_default_constructible_v); spdlog::debug("It is no-throw default constructible.\n"); } THEN("It is copy constructible.") { - REQUIRE(is_copy_constructible_v>); + REQUIRE(is_copy_constructible_v); spdlog::debug("It is copy constructible.\n"); } THEN("It is copy assignable.") { - REQUIRE(is_copy_assignable_v>); + REQUIRE(is_copy_assignable_v); spdlog::debug("It is copy assignable.\n"); } THEN("It is no-throw move constructible.") { - REQUIRE(is_nothrow_move_constructible_v>); + REQUIRE(is_nothrow_move_constructible_v); spdlog::debug("Small function optimization supported."); spdlog::debug("It is no-throw move constructible.\n"); } THEN("It is no-throw move assignable.") { - REQUIRE(is_nothrow_move_assignable_v>); + REQUIRE(is_nothrow_move_assignable_v); spdlog::debug("It is no-throw move assignable.\n"); } THEN("It is no-throw swappable") { - REQUIRE(is_nothrow_swappable_v>); + REQUIRE(is_nothrow_swappable_v); spdlog::debug("It is no-throw swappable.\n"); } } @@ -76,15 +85,15 @@ SCENARIO("Move type to integer conversion" * spdlog::debug("Move type to integer conversion.\n"); GIVEN("A move type.") { - auto move23 = move_type::TWO_THREE; + auto move23 = MoveType::TWO_THREE; REQUIRE_EQ(as_integer(move23), 0); - auto move32 = move_type::THREE_TWO; + auto move32 = MoveType::THREE_TWO; REQUIRE_EQ(as_integer(move32), 1); - auto move26 = move_type::TWO_SIX; + auto move26 = MoveType::TWO_SIX; REQUIRE_EQ(as_integer(move26), 2); - auto move62 = move_type::SIX_TWO; + auto move62 = MoveType::SIX_TWO; REQUIRE_EQ(as_integer(move62), 3); - auto move44 = move_type::FOUR_FOUR; + auto move44 = MoveType::FOUR_FOUR; REQUIRE_EQ(as_integer(move44), 4); } } @@ -95,20 +104,14 @@ SCENARIO("Integer to move type conversion" * spdlog::debug("Integer to move type conversion.\n"); GIVEN("An integer.") { - auto move_choice = 0; - REQUIRE_EQ(as_move(move_choice), move_type::TWO_THREE); - move_choice = 1; - REQUIRE_EQ(as_move(move_choice), move_type::THREE_TWO); - move_choice = 2; - REQUIRE_EQ(as_move(move_choice), move_type::TWO_SIX); - move_choice = 3; - REQUIRE_EQ(as_move(move_choice), move_type::SIX_TWO); - move_choice = 4; - REQUIRE_EQ(as_move(move_choice), move_type::FOUR_FOUR); - move_choice = -1; - REQUIRE_EQ(as_move(move_choice), move_type::FOUR_FOUR); - move_choice = 5; - REQUIRE_EQ(as_move(move_choice), move_type::FOUR_FOUR); + CHECK_EQ(move_from_index(0), MoveType::TWO_THREE); + CHECK_EQ(move_from_index(1), MoveType::THREE_TWO); + CHECK_EQ(move_from_index(2), MoveType::TWO_SIX); + CHECK_EQ(move_from_index(3), MoveType::SIX_TWO); + CHECK_EQ(move_from_index(4), MoveType::FOUR_FOUR); + CHECK_FALSE(move_from_index(5).has_value()); + CHECK_FALSE( + move_from_index(std::numeric_limits::max()).has_value()); } } @@ -117,7 +120,7 @@ SCENARIO("MoveTracker functionality" * doctest::test_suite("move_tracker")) spdlog::debug("MoveTracker functionality.\n"); GIVEN("A 3D Move_tracker.") { - MoveTracker tracked_moves; + MoveTracker tracked_moves; THEN("There are the correct number of elements.") { REQUIRE_EQ(tracked_moves.size(), NUMBER_OF_3D_MOVES); } THEN("Each element is zero-initialized.") @@ -141,9 +144,10 @@ SCENARIO("MoveTracker functionality" * doctest::test_suite("move_tracker")) } THEN("Moves can be updated and read by move type.") { - tracked_moves[move_type::SIX_TWO] = 2; - MoveTracker const& read_only_moves = tracked_moves; - CHECK_EQ(read_only_moves[move_type::SIX_TWO], 2); + tracked_moves[MoveType::SIX_TWO] = 2; + MoveTracker const& read_only_moves = tracked_moves; + CHECK_EQ(read_only_moves[MoveType::SIX_TWO], 2); + CHECK_EQ(read_only_moves[gsl::index{3}], 2); CHECK_EQ(tracked_moves.six_two_moves(), 2); } THEN("Two move trackers can be added.") @@ -154,7 +158,7 @@ SCENARIO("MoveTracker functionality" * doctest::test_suite("move_tracker")) tracked_moves.two_six_moves() += 1; tracked_moves.six_two_moves() += 1; tracked_moves.four_four_moves() += 1; - MoveTracker added_moves; + MoveTracker added_moves; added_moves.two_three_moves() += 2; added_moves.three_two_moves() += 2; added_moves.two_six_moves() += 2; diff --git a/tests/Parallel_triangulation_test.cpp b/tests/Parallel_triangulation_test.cpp index 7357bbe6b..d797e4564 100644 --- a/tests/Parallel_triangulation_test.cpp +++ b/tests/Parallel_triangulation_test.cpp @@ -8,13 +8,22 @@ /// @brief Configuration contract for CGAL's oneTBB triangulation path #include +#include +#include #include +#include #include +#include #include #include +#include +#include #include #include +#include +#include +#include #include #include #include @@ -32,6 +41,55 @@ static_assert(!std::is_aggregate_v>); static_assert(std::copyable>); static_assert(std::is_nothrow_move_constructible_v>); +namespace +{ + [[nodiscard]] auto environment_value(char const* const name) + -> std::optional + { +#if defined(_MSC_VER) + char* raw{}; + std::size_t size{}; + if (auto const error = _dupenv_s(&raw, &size, name); error != 0) + { + throw std::runtime_error{std::string{"Could not read "} + name}; + } + std::unique_ptr value{raw, &std::free}; + if (raw == nullptr) { return std::nullopt; } + return std::string{raw}; +#else + auto const* const raw = std::getenv(name); + if (raw == nullptr) { return std::nullopt; } + return std::string{raw}; +#endif + } + + template + [[nodiscard]] auto positive_environment(char const* const name, + Value const fallback) -> Value + { + auto const raw = environment_value(name); + if (!raw) { return fallback; } + + auto const text = std::string_view{*raw}; + Value value{}; + auto const [end, error] = + std::from_chars(text.data(), text.data() + text.size(), value); + if (error != std::errc{} || end != text.data() + text.size() || value == 0) + { + throw std::invalid_argument{std::string{name} + + " must be a positive integer"}; + } + return value; + } + + [[nodiscard]] auto default_stress_threads() -> std::size_t + { + auto const available = std::thread::hardware_concurrency(); + if (available == 0) { return 2; } + return std::clamp(available, 2, 4); + } +} // namespace + TEST_CASE("Lock-grid bounds fold point ranges into one padded value" * doctest::test_suite("parallel_triangulation")) { @@ -51,183 +109,296 @@ TEST_CASE("Lock-grid bounds fold point ranges into one padded value" * CHECK(detail::locking_box<3>(vertices) == expected); } -TEST_CASE("CGAL parallel insertion and removal retain their lock-grid owner" * - doctest::test_suite("parallel_triangulation")) +SCENARIO("CGAL parallel insertion and removal retain their lock-grid owner" * + doctest::test_suite("parallel_triangulation")) { - Random generator{74}; - std::uniform_real_distribution coordinate{-10.0, 10.0}; - Causal_vertices_t<3> vertices; - vertices.reserve(256); - for (Int_precision index = 0; index < 256; ++index) + GIVEN("A deterministic labelled point range") { - vertices.emplace_back( - Point_t<3>{coordinate(generator), coordinate(generator), - coordinate(generator)}, - index % 4 + 1); - } + Random generator{74}; + std::uniform_real_distribution coordinate{-10.0, 10.0}; + Causal_vertices_t<3> vertices; + vertices.reserve(256); + for (Int_precision index = 0; index < 256; ++index) + { + vertices.emplace_back( + Point_t<3>{coordinate(generator), coordinate(generator), + coordinate(generator)}, + index % 4 + 1); + } - auto triangulation = [&vertices] { detail::Delaunay_state<3> state{vertices}; REQUIRE(state.lock_data_structure() != nullptr); - CHECK(state.has_consistent_lock_binding()); - CHECK(state.triangulation().is_parallel()); - CHECK(detail::locking_box<3>(state.triangulation()) == - detail::locking_box<3>(vertices)); REQUIRE(state.triangulation().number_of_vertices() > 100); REQUIRE(state.triangulation().is_valid()); - test_helpers::expect_labels_preserved(state.triangulation(), vertices); - detail::Delaunay_state<3> const copied{state}; - REQUIRE(copied.lock_data_structure() != nullptr); - CHECK(copied.lock_data_structure() != state.lock_data_structure()); - CHECK(copied.has_consistent_lock_binding()); - CHECK(copied.triangulation().is_valid()); + WHEN("The state is copied, moved, range-mutated, and published") + { + auto* const original_lock = state.lock_data_structure(); + detail::Delaunay_state<3> const copied{state}; + detail::Delaunay_state<3> moved{std::move(state)}; + REQUIRE(copied.lock_data_structure() != nullptr); + REQUIRE(moved.lock_data_structure() != nullptr); - detail::Delaunay_state<3> moved{std::move(state)}; - REQUIRE(moved.lock_data_structure() != nullptr); - CHECK(state.lock_data_structure() == nullptr); - CHECK(state.has_consistent_lock_binding()); - CHECK(moved.has_consistent_lock_binding()); + auto& delaunay = moved.mutable_triangulation_unchecked(); + std::vector> vertices_to_remove; + vertices_to_remove.reserve(16); + for (auto const vertex : + delaunay.finite_vertex_handles() | std::views::take(16)) + { + vertices_to_remove.emplace_back(vertex); + } - auto& delaunay = moved.mutable_triangulation_unchecked(); - std::vector> vertices_to_remove; - vertices_to_remove.reserve(16); - for (auto const vertex : - delaunay.finite_vertex_handles() | std::views::take(16)) - { - vertices_to_remove.emplace_back(vertex); - } + auto const removed = + delaunay.remove(vertices_to_remove.begin(), vertices_to_remove.end()); + FoliatedTriangulation_3 triangulation{ + Delaunay_t<3>{moved.triangulation()}, 0.0, 1.0}; + auto snapshot = triangulation.delaunay_snapshot(); + + THEN("Each owner remains valid and public snapshots detach the lock grid") + { + CHECK(state.lock_data_structure() == nullptr); + CHECK(state.has_consistent_lock_binding()); + + CHECK(copied.lock_data_structure() != original_lock); + CHECK(copied.has_consistent_lock_binding()); + CHECK(copied.triangulation().is_parallel()); + CHECK(copied.triangulation().is_valid()); + CHECK(detail::locking_box<3>(copied.triangulation()) == + detail::locking_box<3>(vertices)); + test_helpers::expect_labels_preserved(copied.triangulation(), vertices); - CHECK( - delaunay.remove(vertices_to_remove.begin(), vertices_to_remove.end()) == - vertices_to_remove.size()); - CHECK(moved.has_consistent_lock_binding()); - CHECK(delaunay.is_valid()); - - return FoliatedTriangulation_3{Delaunay_t<3>{moved.triangulation()}, 0.0, - 1.0}; - }(); - CHECK(triangulation.is_delaunay()); - - auto snapshot = triangulation.delaunay_snapshot(); - CHECK_FALSE(snapshot.is_parallel()); - CHECK(snapshot.get_lock_data_structure() == nullptr); - CHECK(snapshot.is_valid()); + CHECK(moved.lock_data_structure() == original_lock); + CHECK(moved.has_consistent_lock_binding()); + CHECK_EQ(removed, vertices_to_remove.size()); + CHECK(delaunay.is_valid()); + + CHECK(triangulation.is_delaunay()); + CHECK_FALSE(snapshot.is_parallel()); + CHECK(snapshot.get_lock_data_structure() == nullptr); + CHECK(snapshot.is_valid()); + } + } + } } -TEST_CASE("A failed concurrent lock leaves the triangulation unchanged" * - doctest::test_suite("parallel_triangulation")) +SCENARIO("A failed concurrent lock leaves the triangulation unchanged" * + doctest::test_suite("parallel_triangulation")) { - Causal_vertices_t<3> const vertices{ - {Point_t<3>{-1.0, -1.0, -1.0}, 1}, - { Point_t<3>{1.0, -1.0, -1.0}, 1}, - { Point_t<3>{-1.0, 1.0, -1.0}, 2}, - { Point_t<3>{-1.0, -1.0, 1.0}, 2}, - { Point_t<3>{1.0, 1.0, 1.0}, 3}, - }; - detail::Delaunay_state<3> state{vertices}; - auto& triangulation = state.mutable_triangulation_unchecked(); - auto* const lock_grid = state.lock_data_structure(); - REQUIRE(lock_grid != nullptr); - - auto const start = *triangulation.finite_cell_handles().begin(); - auto const contested_point = start->vertex(0)->point(); - Point_t<3> const candidate{contested_point.x() + 0.01, - contested_point.y() + 0.01, - contested_point.z() + 0.01}; - REQUIRE(lock_grid->check_if_all_tls_cells_are_unlocked()); - REQUIRE(lock_grid->template try_lock(contested_point)); - REQUIRE_MESSAGE( - lock_grid->is_locked_by_this_thread(candidate), - "Contention fixture requires candidate and contested_point to share a " - "lock-grid cell; update them when LOCK_GRID_RESOLUTION or " - "GV_BOUNDING_BOX_SIZE changes."); - lock_grid->unlock_all_points_locked_by_this_thread(); + GIVEN("A candidate whose lock-grid cell is held by another thread") + { + Causal_vertices_t<3> const vertices{ + {Point_t<3>{-1.0, -1.0, -1.0}, 1}, + { Point_t<3>{1.0, -1.0, -1.0}, 1}, + { Point_t<3>{-1.0, 1.0, -1.0}, 2}, + { Point_t<3>{-1.0, -1.0, 1.0}, 2}, + { Point_t<3>{1.0, 1.0, 1.0}, 3}, + }; + detail::Delaunay_state<3> state{vertices}; + auto& triangulation = state.mutable_triangulation_unchecked(); + auto* const lock_grid = state.lock_data_structure(); + REQUIRE(lock_grid != nullptr); + + auto const start = *triangulation.finite_cell_handles().begin(); + auto const contested_point = start->vertex(0)->point(); + Point_t<3> const candidate{contested_point.x() + 0.01, + contested_point.y() + 0.01, + contested_point.z() + 0.01}; + REQUIRE(lock_grid->check_if_all_tls_cells_are_unlocked()); + REQUIRE(lock_grid->template try_lock(contested_point)); + REQUIRE_MESSAGE( + lock_grid->is_locked_by_this_thread(candidate), + "Contention fixture requires candidate and contested_point to share a " + "lock-grid cell; update them when LOCK_GRID_RESOLUTION or " + "GV_BOUNDING_BOX_SIZE changes."); + lock_grid->unlock_all_points_locked_by_this_thread(); + + WHEN("Insertion tries to acquire the contested zone") + { + auto const vertices_before = triangulation.number_of_vertices(); + auto const cells_before = triangulation.number_of_finite_cells(); + bool could_lock_zone = true; + Vertex_handle_t<3> inserted; + { + std::latch locked{1}; + std::latch release{1}; + std::atomic_bool lock_acquired{false}; + std::jthread holder{[&] { + lock_acquired.store( + lock_grid->template try_lock(contested_point)); + locked.count_down(); + release.wait(); + lock_grid->unlock_all_points_locked_by_this_thread(); + }}; + [[maybe_unused]] auto const release_holder = + gsl::finally([&release] { release.count_down(); }); + locked.wait(); + if (!lock_acquired.load()) + { + FAIL_CHECK( + "The fixture thread could not acquire the candidate lock."); + return; + } + inserted = triangulation.insert(candidate, start, &could_lock_zone); + triangulation.unlock_all_elements(); + } + + THEN("Insertion fails atomically and every lock is released") + { + CHECK_FALSE(could_lock_zone); + CHECK(inserted == Vertex_handle_t<3>{}); + CHECK_EQ(triangulation.number_of_vertices(), vertices_before); + CHECK_EQ(triangulation.number_of_finite_cells(), cells_before); + CHECK_FALSE(find_vertex<3>(triangulation, candidate)); + CHECK(triangulation.is_valid()); + CHECK(lock_grid->check_if_all_cells_are_unlocked()); + } + } + } +} + +SCENARIO("Parallel lock ownership survives wrapper value transfers" * + doctest::test_suite("parallel_triangulation")) +{ + GIVEN("A parallel wrapper and a verifier that runs after donor destruction") { - std::latch locked{1}; - std::latch release{1}; - std::atomic_bool lock_acquired{false}; - std::jthread holder{[&] { - lock_acquired.store(lock_grid->template try_lock(contested_point)); - locked.count_down(); - release.wait(); - lock_grid->unlock_all_points_locked_by_this_thread(); - }}; - [[maybe_unused]] auto const release_holder = - gsl::finally([&release] { release.count_down(); }); - locked.wait(); - if (!lock_acquired.load()) + Causal_vertices_t<3> const vertices{ + {Point_t<3>{1.0, 0.0, 0.0}, 1}, + {Point_t<3>{0.0, 1.0, 0.0}, 1}, + {Point_t<3>{0.0, 0.0, 1.0}, 1}, + {Point_t<3>{0.0, 0.0, 2.0}, 2}, + {Point_t<3>{2.0, 0.0, 0.0}, 2}, + {Point_t<3>{0.0, 3.0, 0.0}, 3}, + }; + FoliatedTriangulation_3 const original{vertices}; + + auto verify_after_donor_destruction = + [](FoliatedTriangulation_3& candidate) { + CHECK_FALSE(candidate.is_initialized()); + CHECK(candidate.is_fixed()); + CHECK(candidate.is_initialized()); + CHECK(candidate.number_of_vertices() == 5); + auto snapshot = candidate.delaunay_snapshot(); + CHECK_FALSE(snapshot.is_parallel()); + CHECK(snapshot.get_lock_data_structure() == nullptr); + CHECK(snapshot.is_valid()); + }; + + WHEN("A copy-constructed value outlives its donor") { - FAIL_CHECK("The fixture thread could not acquire the candidate lock."); - return; + auto candidate = [&original] { + FoliatedTriangulation_3 donor{original}; + return FoliatedTriangulation_3{donor}; + }(); + + THEN("The value retains usable ownership") + { verify_after_donor_destruction(candidate); } } - auto const vertices_before = triangulation.number_of_vertices(); - auto const cells_before = triangulation.number_of_finite_cells(); - bool could_lock_zone = true; - auto const inserted = - triangulation.insert(candidate, start, &could_lock_zone); - - CHECK_FALSE(could_lock_zone); - CHECK(inserted == Vertex_handle_t<3>{}); - CHECK_EQ(triangulation.number_of_vertices(), vertices_before); - CHECK_EQ(triangulation.number_of_finite_cells(), cells_before); - CHECK_FALSE(find_vertex<3>(triangulation, candidate)); - CHECK(triangulation.is_valid()); + WHEN("A move-constructed value outlives its donor") + { + auto candidate = [&original] { + FoliatedTriangulation_3 donor{original}; + return FoliatedTriangulation_3{std::move(donor)}; + }(); + + THEN("The value retains usable ownership") + { verify_after_donor_destruction(candidate); } + } + + WHEN("A copy-assigned value outlives its donor") + { + FoliatedTriangulation_3 candidate; + { + FoliatedTriangulation_3 const donor{original}; + candidate = donor; + } - triangulation.unlock_all_elements(); + THEN("The value retains usable ownership") + { verify_after_donor_destruction(candidate); } + } + + WHEN("A move-assigned value outlives its donor") + { + FoliatedTriangulation_3 candidate; + { + FoliatedTriangulation_3 donor{original}; + candidate = std::move(donor); + } + + THEN("The value retains usable ownership") + { verify_after_donor_destruction(candidate); } + } } - CHECK(lock_grid->check_if_all_cells_are_unlocked()); } -TEST_CASE("Parallel lock ownership survives wrapper value transfers" * +TEST_CASE("Parallel insertion and removal stress is replayable" * doctest::test_suite("parallel_triangulation")) { - Causal_vertices_t<3> const vertices{ - {Point_t<3>{1.0, 0.0, 0.0}, 1}, - {Point_t<3>{0.0, 1.0, 0.0}, 1}, - {Point_t<3>{0.0, 0.0, 1.0}, 1}, - {Point_t<3>{0.0, 0.0, 2.0}, 2}, - {Point_t<3>{2.0, 0.0, 0.0}, 2}, - {Point_t<3>{0.0, 3.0, 0.0}, 3}, - }; - FoliatedTriangulation_3 const original{vertices}; - - auto verify_after_donor_destruction = [](FoliatedTriangulation_3& candidate) { - CHECK_FALSE(candidate.is_initialized()); - CHECK(candidate.is_fixed()); - CHECK(candidate.is_initialized()); - CHECK(candidate.number_of_vertices() == 5); - auto snapshot = candidate.delaunay_snapshot(); - CHECK_FALSE(snapshot.is_parallel()); - CHECK(snapshot.get_lock_data_structure() == nullptr); - CHECK(snapshot.is_valid()); - }; - - auto copy_constructed = [&original] { - FoliatedTriangulation_3 donor{original}; - return FoliatedTriangulation_3{donor}; - }(); - verify_after_donor_destruction(copy_constructed); + auto const seed = RandomSeed{positive_environment( + "CDT_PARALLEL_TEST_SEED", std::uint64_t{88})}; + auto const thread_count = positive_environment( + "CDT_PARALLEL_TEST_THREADS", default_stress_threads()); + auto const iterations = positive_environment( + "CDT_PARALLEL_TEST_ITERATIONS", std::size_t{4}); + constexpr auto point_count = std::size_t{1'024}; + CAPTURE(seed); + CAPTURE(thread_count); + CAPTURE(iterations); + CAPTURE(point_count); + REQUIRE_MESSAGE( + thread_count >= 2, + "CDT_PARALLEL_TEST_THREADS must be at least 2 so this launcher exercises " + "the parallel configuration."); - auto move_constructed = [&original] { - FoliatedTriangulation_3 donor{original}; - return FoliatedTriangulation_3{std::move(donor)}; - }(); - verify_after_donor_destruction(move_constructed); + oneapi::tbb::global_control thread_limit{ + oneapi::tbb::global_control::max_allowed_parallelism, thread_count}; + CHECK_EQ(oneapi::tbb::global_control::active_value( + oneapi::tbb::global_control::max_allowed_parallelism), + thread_count); - FoliatedTriangulation_3 copy_assigned; + for (std::size_t iteration = 0; iteration < iterations; ++iteration) { - FoliatedTriangulation_3 const donor{original}; - copy_assigned = donor; - } - verify_after_donor_destruction(copy_assigned); + CAPTURE(iteration); + auto const stream = + RandomStream{gsl::narrow(iteration + 88)}; + Random generator{seed, stream}; + CAPTURE(stream); + std::uniform_real_distribution coordinate{-100.0, 100.0}; + Causal_vertices_t<3> vertices; + vertices.reserve(point_count); + for (std::size_t index = 0; index < point_count; ++index) + { + vertices.emplace_back( + Point_t<3>{coordinate(generator), coordinate(generator), + coordinate(generator)}, + gsl::narrow(index % 4 + 1)); + } - FoliatedTriangulation_3 move_assigned; - { - FoliatedTriangulation_3 donor{original}; - move_assigned = std::move(donor); + detail::Delaunay_state<3> state{vertices}; + REQUIRE(state.has_consistent_lock_binding()); + REQUIRE(state.lock_data_structure() != nullptr); + auto& triangulation = state.mutable_triangulation_unchecked(); + CHECK(triangulation.is_parallel()); + REQUIRE_EQ(triangulation.number_of_vertices(), point_count); + REQUIRE(triangulation.is_valid()); + test_helpers::expect_labels_preserved(triangulation, vertices); + + auto const removal_count = point_count / 8; + std::vector> vertices_to_remove; + vertices_to_remove.reserve(removal_count); + for (auto const vertex : triangulation.finite_vertex_handles() | + std::views::take(removal_count)) + { + vertices_to_remove.emplace_back(vertex); + } + + REQUIRE_EQ(triangulation.remove(vertices_to_remove.begin(), + vertices_to_remove.end()), + removal_count); + CHECK_EQ(triangulation.number_of_vertices(), point_count - removal_count); + CHECK(state.has_consistent_lock_binding()); + CHECK(triangulation.is_valid()); + CHECK(state.lock_data_structure()->check_if_all_cells_are_unlocked()); } - verify_after_donor_destruction(move_assigned); } diff --git a/tests/Public_api_consumer.cpp b/tests/Public_api_consumer.cpp index fd8eb895c..7a264a0bc 100644 --- a/tests/Public_api_consumer.cpp +++ b/tests/Public_api_consumer.cpp @@ -1,9 +1,10 @@ #include +#include #include -#include +#include #include -#include #include +#include #include "Ergodic_moves_3.hpp" #include "Foliated_triangulation.hpp" @@ -18,12 +19,32 @@ using Manifold = cdt::manifolds::Manifold_3; using Move_command = cdt::MoveCommand; using Move_result = cdt::ergodic_moves::MoveResult; -template -concept Supported_move_command_result = - requires { typename cdt::MoveCommand; }; +template +concept CompleteMoveStrategy = + requires { sizeof(cdt::MoveStrategy); }; + +template +concept IntegerRandomSample = requires(cdt::Random& random, Number value) { + cdt::utilities::generate_random_int(random, value, value); +}; + +template +concept RealRandomSample = requires(cdt::Random& random, Number value) { + cdt::utilities::generate_random_real(random, value, value); +}; static_assert(std::same_as); static_assert(std::uniform_random_bit_generator); +static_assert(!std::convertible_to); +static_assert(!std::convertible_to); +static_assert( + std::constructible_from); +static_assert( + !std::constructible_from); +static_assert(IntegerRandomSample); +static_assert(!IntegerRandomSample); +static_assert(RealRandomSample); +static_assert(!RealRandomSample); static_assert(std::same_as>); static_assert( std::same_as(2, 2, 1.0, 1.0, random) } -> std::same_as>; }); -static_assert( - std::same_as>); -static_assert( - std::same_as>); +static_assert(requires(cdt::Delaunay_t<3>& triangulation, + cdt::Edge_handle_t<3> const& edge) { + { + cdt::foliated_triangulations::classify_edge<3>(edge) + } -> std::same_as; + { + cdt::foliated_triangulations::fix_cells<3>(triangulation) + } -> std::same_as; +}); +static_assert(std::same_as< + cdt::MoveAlways_3, + cdt::MoveStrategy>); +static_assert(std::same_as< + cdt::Metropolis_3, + cdt::MoveStrategy>); +static_assert(!std::default_initializable); static_assert(std::is_constructible_v); -static_assert( - std::same_as>); -static_assert(Supported_move_command_result); -static_assert( - !Supported_move_command_result>); +static_assert(CompleteMoveStrategy); +static_assert(CompleteMoveStrategy); +static_assert(!CompleteMoveStrategy(255)>); +static_assert(requires(Move_command& command, + Move_command const& const_command) { + { command.result() } noexcept -> std::same_as; + { const_command.result() } noexcept -> std::same_as; + { std::move(command).result() } noexcept -> std::same_as; +}); static_assert(requires { { cdt::MoveRunCadence::parse(1, 1) } -> std::same_as>; }); +static_assert(requires { + { + cdt::move_tracker::move_from_index(0) + } -> std::same_as>; + { + cdt::Metropolis_3::reverse_move(cdt::move_tracker::MoveType::TWO_THREE) + } -> std::same_as>; +}); static_assert(requires(Manifold const& manifold, cdt::Random& random) { { cdt::ergodic_moves::null_move(manifold) } -> std::same_as; @@ -91,3 +134,24 @@ static_assert(requires { cdt::s3_action::make_physical_parameters(0.6L, 1.1L, 0.1L) } -> std::same_as; }); +static_assert(!std::is_aggregate_v); +static_assert(!std::is_constructible_v); +static_assert(requires(cdt::s3_action::PhysicalParameters const& parameters) { + { parameters.alpha() } noexcept -> std::same_as; + { parameters.k() } noexcept -> std::same_as; + { parameters.lambda() } noexcept -> std::same_as; + { + cdt::s3_action::s3_bulk_action(1, 1, 1, parameters) + } -> std::same_as; +}); + +static_assert(requires { + { + cdt::runtime_config::make_triangulation(true, false, 64, 3, 3, 1.0, 1.0, + cdt::RandomSeed{92}, 4) + } -> std::same_as; +}); +static_assert(requires(cdt::runtime_config::Triangulation const& config) { + { config.threads() } noexcept -> std::same_as; +}); diff --git a/tests/Random_benchmark.cpp b/tests/Random_benchmark.cpp index 695ad8cf1..35041327c 100644 --- a/tests/Random_benchmark.cpp +++ b/tests/Random_benchmark.cpp @@ -16,6 +16,7 @@ #include #include "Move_tracker.hpp" +#include "Random.hpp" using namespace cdt; diff --git a/tests/Random_test.cpp b/tests/Random_test.cpp index b0bf96dc9..e21d50749 100644 --- a/tests/Random_test.cpp +++ b/tests/Random_test.cpp @@ -23,7 +23,7 @@ using namespace cdt; SCENARIO("PCG runs are reproducible and independently split" * doctest::test_suite("random")) { - auto constexpr seed = cdt::Random_seed{92}; + constexpr auto seed = cdt::RandomSeed{92}; CAPTURE(seed); GIVEN("Two engines with the same seed and stream") @@ -70,8 +70,8 @@ SCENARIO("PCG runs are reproducible and independently split" * SCENARIO("Distinct seeds have pinned PCG transition prefixes" * doctest::test_suite("random")) { - auto constexpr seeds = std::array{cdt::Random_seed{92}, cdt::Random_seed{93}}; - auto constexpr expected = std::array{ + constexpr auto seeds = std::array{cdt::RandomSeed{92}, cdt::RandomSeed{93}}; + constexpr auto expected = std::array{ std::array{6582339598257575626ULL, 18421894668611219029ULL, 5474286625189102014ULL}, std::array{8015168658319708503ULL, diff --git a/tests/Runtime_config_test.cpp b/tests/Runtime_config_test.cpp index b2cc632a3..fc5137532 100644 --- a/tests/Runtime_config_test.cpp +++ b/tests/Runtime_config_test.cpp @@ -18,17 +18,15 @@ using namespace cdt; namespace { - auto test_make_triangulation(bool const spherical, bool const toroidal, - long long const simplices, - long long const timeslices, - long long const dimensions, - double const initial_radius, - double const foliation_spacing) - -> runtime_config::Triangulation + auto test_make_triangulation( + bool const spherical, bool const toroidal, long long const simplices, + long long const timeslices, long long const dimensions, + double const initial_radius, double const foliation_spacing, + long long const threads = 1) -> runtime_config::Triangulation { return runtime_config::make_triangulation( spherical, toroidal, simplices, timeslices, dimensions, initial_radius, - foliation_spacing); + foliation_spacing, cdt::RandomSeed{}, threads); } auto test_make_simulation(runtime_config::Triangulation const& triangulation, @@ -44,9 +42,9 @@ namespace static_assert(!std::is_aggregate_v); static_assert(!std::is_default_constructible_v); -static_assert(!std::is_constructible_v< - runtime_config::Triangulation, topology_type, Int_precision, - Int_precision, Int_precision, double, double>); +static_assert(!std::is_constructible_v); static_assert(!std::is_aggregate_v); static_assert(!std::is_default_constructible_v); static_assert( @@ -66,13 +64,14 @@ SCENARIO("Runtime triangulation options parse into a validated value" * THEN("The narrowed value contains the supported configuration.") { - CHECK_EQ(config.topology(), topology_type::SPHERICAL); + CHECK_EQ(config.topology(), Topology::SPHERICAL); CHECK_EQ(config.simplices(), 64); CHECK_EQ(config.timeslices(), 3); CHECK_EQ(config.dimensions(), 3); CHECK_EQ(config.initial_radius(), 1.0); CHECK_EQ(config.foliation_spacing(), 1.0); - CHECK_EQ(config.seed(), 0); + CHECK_EQ(config.seed(), cdt::RandomSeed{}); + CHECK_EQ(config.threads(), 1); } } } @@ -167,6 +166,43 @@ SCENARIO("Runtime triangulation options parse into a validated value" * } } } + + GIVEN("Raw Delaunay thread limits.") + { + WHEN("A nonpositive thread limit is supplied.") + { + THEN("The invalid resource limit is rejected before storage.") + { + CHECK_THROWS_WITH_AS( + test_make_triangulation(true, false, 64, 3, 3, 1.0, 1.0, 0), + "Thread count must be positive.", std::invalid_argument); + CHECK_THROWS_WITH_AS( + test_make_triangulation(true, false, 64, 3, 3, 1.0, 1.0, -1), + "Thread count must be positive.", std::invalid_argument); + } + } + + WHEN("More than one thread is requested.") + { +#if defined(CDT_ENABLE_PARALLEL_TRIANGULATION) && \ + CDT_ENABLE_PARALLEL_TRIANGULATION + auto const config = + test_make_triangulation(true, false, 64, 3, 3, 1.0, 1.0, 4); + + THEN("The parallel build retains the validated maximum.") + { CHECK_EQ(config.threads(), 4); } +#else + THEN("The sequential build rejects unsupported parallelism.") + { + CHECK_THROWS_WITH_AS( + test_make_triangulation(true, false, 64, 3, 3, 1.0, 1.0, 4), + "This build supports only --threads 1; use the parallel preset for " + "larger values.", + std::invalid_argument); + } +#endif + } + } } SCENARIO("Simulation options parse into a validated value" * @@ -196,10 +232,10 @@ SCENARIO("Simulation options parse into a validated value" * WHEN("An explicit root seed is parsed with the triangulation options.") { auto const seeded_triangulation = runtime_config::make_triangulation( - true, false, 64, 3, 3, 1.0, 1.0, cdt::Random_seed{92}); + true, false, 64, 3, 3, 1.0, 1.0, cdt::RandomSeed{92}); THEN("The effective seed is retained by validated configuration.") - { CHECK_EQ(seeded_triangulation.seed(), 92); } + { CHECK_EQ(seeded_triangulation.seed(), cdt::RandomSeed{92}); } } WHEN("Alpha is outside its physical domain.") { diff --git a/tests/S3Action_test.cpp b/tests/S3Action_test.cpp index 29eb1f137..378ef3346 100644 --- a/tests/S3Action_test.cpp +++ b/tests/S3Action_test.cpp @@ -27,6 +27,70 @@ using namespace manifolds; static_assert(std::is_nothrow_destructible_v); +// Anonymous by design: keep the independent reference oracles local to this +// test translation unit. +namespace +{ + [[nodiscard]] auto alpha_minus_one_reference(Int_precision const n1_tl, + Int_precision const n3_31_13, + Int_precision const n3_22, + long double const k, + long double const lambda) + -> long double + { + auto const n1 = static_cast(n1_tl); + auto const n31 = static_cast(n3_31_13); + auto const n22 = static_cast(n3_22); + return -2.0L * std::numbers::pi_v * k * n1 + + n31 * (2.673L * k + 0.118L * lambda) + + n22 * (7.386L * k + 0.118L * lambda); + } + + [[nodiscard]] auto alpha_one_reference(Int_precision const n1_tl, + Int_precision const n3_31_13, + Int_precision const n3_22, + long double const k, + long double const lambda) + -> long double + { + auto const n1 = static_cast(n1_tl); + auto const n31 = static_cast(n3_31_13); + auto const n22 = static_cast(n3_22); + return 2.0L * std::numbers::pi_v * k * n1 + + n31 * (-3.548L * k - 0.167L * lambda) + + n22 * (-5.355L * k - 0.204L * lambda); + } + + [[nodiscard]] auto generalized_reference( + 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) -> long double + { + auto const n1 = static_cast(n1_tl); + auto const n31 = static_cast(n3_31_13); + auto const n22 = static_cast(n3_22); + auto const sqrt_alpha = std::sqrt(alpha); + auto const alpha_denominator = 4.0L * alpha + 1.0L; + + auto const three_one = + -3.0L * k * + std::asinh(1.0L / + (std::sqrt(3.0L) * std::sqrt(alpha_denominator))) - + 3.0L * k * sqrt_alpha * + std::acos((2.0L * alpha + 1.0L) / alpha_denominator) - + lambda / 12.0L * std::sqrt(3.0L * alpha + 1.0L); + auto const two_two = + 2.0L * k * + std::asinh(2.0L * std::sqrt(2.0L) * std::sqrt(2.0L * alpha + 1.0L) / + alpha_denominator) - + 4.0L * k * sqrt_alpha * std::acos(-1.0L / alpha_denominator) - + lambda / 12.0L * std::sqrt(4.0L * alpha + 2.0L); + + return 2.0L * std::numbers::pi_v * k * sqrt_alpha * n1 + + n31 * three_one + n22 * two_two; + } +} // namespace + SCENARIO("MPFR calculations use scope-owned values" * doctest::test_suite("s3action")) { @@ -53,18 +117,19 @@ SCENARIO("MPFR calculations use scope-owned values" * { THEN("The invalid input is rejected.") { - CHECK_THROWS_AS( + CHECK_THROWS_WITH_AS( static_cast(mpfr_values::from_decimal("not-a-number")), - std::invalid_argument); + "Invalid decimal MPFR value.", std::invalid_argument); } } WHEN("A null decimal pointer is supplied.") { THEN("The invalid boundary is rejected before calling MPFR.") { - CHECK_THROWS_AS(static_cast(mpfr_values::from_decimal( - static_cast(nullptr))), - std::invalid_argument); + CHECK_THROWS_WITH_AS( + static_cast( + mpfr_values::from_decimal(static_cast(nullptr))), + "MPFR decimal value must not be null.", std::invalid_argument); } } } @@ -76,10 +141,10 @@ SCENARIO("Calculate the bulk action on S3 triangulations" * spdlog::debug("Calculate the bulk action on S3 triangulations.\n"); GIVEN("A 3D 2-sphere foliated triangulation.") { - auto constexpr simplices = 6400; - auto constexpr timeslices = 7; - auto constexpr K = 1.1L; // NOLINT - auto constexpr Lambda = 0.1L; + constexpr auto simplices = 6400; + constexpr auto timeslices = 7; + constexpr auto K = 1.1L; // NOLINT + constexpr auto Lambda = 0.1L; Manifold_3 const universe(simplices, timeslices, cdt::Random{92}); // Verify triangulation CHECK_EQ(universe.N3(), universe.simplices()); @@ -92,56 +157,65 @@ SCENARIO("Calculate the bulk action on S3 triangulations" * CHECK_EQ(universe.min_time(), 1); WHEN("The alpha=-1 Bulk Action is calculated.") { - auto Bulk_action = S3_bulk_action_alpha_minus_one( + auto Bulk_action = s3_bulk_action_alpha_minus_one( universe.N1_TL(), universe.N3_31_13(), universe.N3_22(), K, Lambda); - THEN("The action falls within accepted values.") + THEN("The action matches the independent closed-form calculation.") { - spdlog::debug("S3_bulk_action_alpha_minus_one() = {}\n", + spdlog::debug("s3_bulk_action_alpha_minus_one() = {}\n", Bulk_action.to_double()); - REQUIRE_LE(3500, Bulk_action); - REQUIRE_LE(Bulk_action, 4500); + auto const expected = alpha_minus_one_reference( + universe.N1_TL(), universe.N3_31_13(), universe.N3_22(), K, Lambda); + CHECK(mpfr_values::to_long_double(Bulk_action) == + doctest::Approx(expected).epsilon(1e-12)); } } WHEN("The alpha=1 Bulk Action is calculated.") { - auto Bulk_action = S3_bulk_action_alpha_one( + auto Bulk_action = s3_bulk_action_alpha_one( universe.N1_TL(), universe.N3_31_13(), universe.N3_22(), K, Lambda); - THEN("The action falls within accepted values.") + THEN("The action matches the independent closed-form calculation.") { - spdlog::debug("S3_bulk_action_alpha_one() = {}\n", + spdlog::debug("s3_bulk_action_alpha_one() = {}\n", Bulk_action.to_double()); - REQUIRE_LE(2000, Bulk_action); - REQUIRE_LE(Bulk_action, 3000); + auto const expected = alpha_one_reference( + universe.N1_TL(), universe.N3_31_13(), universe.N3_22(), K, Lambda); + CHECK(mpfr_values::to_long_double(Bulk_action) == + doctest::Approx(expected).epsilon(1e-12)); } } WHEN("The generalized Bulk Action is calculated.") { - auto constexpr Alpha = 0.6L; + constexpr auto Alpha = 0.6L; spdlog::debug("(Long double) Alpha = {}\n", Alpha); - auto Bulk_action = S3_bulk_action(universe.N1_TL(), universe.N3_31_13(), - universe.N3_22(), Alpha, K, Lambda); - THEN("The action falls within accepted values.") + auto const parameters = make_physical_parameters(Alpha, K, Lambda); + auto Bulk_action = s3_bulk_action(universe.N1_TL(), universe.N3_31_13(), + universe.N3_22(), parameters); + THEN("The action matches the independent closed-form calculation.") { - spdlog::debug("S3_bulk_action() = {}\n", Bulk_action.to_double()); - REQUIRE_LE(2700, Bulk_action); - REQUIRE_LE(Bulk_action, 3700); + spdlog::debug("s3_bulk_action() = {}\n", Bulk_action.to_double()); + auto const expected = + generalized_reference(universe.N1_TL(), universe.N3_31_13(), + universe.N3_22(), Alpha, K, Lambda); + CHECK(mpfr_values::to_long_double(Bulk_action) == + doctest::Approx(expected).epsilon(1e-12)); } } WHEN( - "S3_bulk_action(alpha=1) and S3_bulk_action_alpha_one() are " + "s3_bulk_action(alpha=1) and s3_bulk_action_alpha_one() are " "calculated.") { - auto constexpr Alpha = 1.0L; - auto Bulk_action = S3_bulk_action(universe.N1_TL(), universe.N3_31_13(), - universe.N3_22(), Alpha, K, Lambda); - auto Bulk_action_one = S3_bulk_action_alpha_one( + constexpr auto Alpha = 1.0L; + auto const parameters = make_physical_parameters(Alpha, K, Lambda); + auto Bulk_action = s3_bulk_action(universe.N1_TL(), universe.N3_31_13(), + universe.N3_22(), parameters); + auto Bulk_action_one = s3_bulk_action_alpha_one( universe.N1_TL(), universe.N3_31_13(), universe.N3_22(), K, Lambda); THEN( - "S3_bulk_action(alpha=1) == S3_bulk_action_alpha_one() within " + "s3_bulk_action(alpha=1) == s3_bulk_action_alpha_one() within " "tolerances.") { - spdlog::debug("S3_bulk_action() = {}\n", Bulk_action.to_double()); - spdlog::debug("S3_bulk_action_alpha_one() = {}\n", + spdlog::debug("s3_bulk_action() = {}\n", Bulk_action.to_double()); + spdlog::debug("s3_bulk_action_alpha_one() = {}\n", Bulk_action_one.to_double()); REQUIRE(mpfr_values::to_double(Bulk_action_one) == doctest::Approx(mpfr_values::to_double(Bulk_action)) @@ -156,12 +230,12 @@ SCENARIO("Bulk action precision survives the acceptance boundary" * { 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 = + constexpr auto 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, + 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( + 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); @@ -187,38 +261,38 @@ SCENARIO("Bulk action rejects invalid physical parameters" * { WHEN("Alpha is at the lower domain boundary.") { - THEN("The generalized action rejects it with a domain error.") + THEN("Physical-parameter construction rejects it with a domain error.") { - CHECK_THROWS_AS( - static_cast(S3_bulk_action(1, 1, 1, 0.5L, 1.1L, 0.1L)), - std::domain_error); + CHECK_THROWS_WITH_AS( + static_cast(make_physical_parameters(0.5L, 1.1L, 0.1L)), + "Alpha in 3D must be greater than 1/2.", std::domain_error); } } WHEN("Alpha is not finite.") { - THEN("The generalized action rejects it as invalid input.") + THEN("Physical-parameter construction rejects it as invalid input.") { - CHECK_THROWS_AS( - static_cast(S3_bulk_action( - 1, 1, 1, std::numeric_limits::quiet_NaN(), 1.1L, - 0.1L)), - std::invalid_argument); + CHECK_THROWS_WITH_AS( + static_cast(make_physical_parameters( + std::numeric_limits::quiet_NaN(), 1.1L, 0.1L)), + "Physical parameters must be finite.", std::invalid_argument); } } WHEN("A specialized action receives an infinite coupling.") { THEN("The specialized action rejects it as invalid input.") { - CHECK_THROWS_AS( - static_cast(S3_bulk_action_alpha_one( + CHECK_THROWS_WITH_AS( + static_cast(s3_bulk_action_alpha_one( 1, 1, 1, std::numeric_limits::infinity(), 0.1L)), - std::invalid_argument); + "Physical parameters must be finite.", std::invalid_argument); } } WHEN("The generalized action's exception specification is examined.") { - THEN("It permits parameter validation to throw.") - { CHECK_FALSE(noexcept(S3_bulk_action(1, 1, 1, 0.6L, 1.1L, 0.1L))); } + auto const parameters = make_physical_parameters(0.6L, 1.1L, 0.1L); + THEN("It permits MPFR operations to report failures.") + { CHECK_FALSE(noexcept(s3_bulk_action(1, 1, 1, parameters))); } } } } diff --git a/tests/Settings_test.cpp b/tests/Settings_test.cpp index 0f930a4fd..fdad396b6 100644 --- a/tests/Settings_test.cpp +++ b/tests/Settings_test.cpp @@ -35,9 +35,9 @@ SCENARIO("Check settings" * doctest::test_suite("settings")) } WHEN("Memory alignment is queried.") { - auto constexpr align_64 = ALIGNMENT_64_BIT; + constexpr auto align_64 = ALIGNMENT_64_BIT; THEN("The value is 64 bits.") { REQUIRE_EQ(align_64, 64); } - auto constexpr align_32 = ALIGNMENT_32_BIT; + constexpr auto align_32 = ALIGNMENT_32_BIT; THEN("The value is 32 bits.") { REQUIRE_EQ(align_32, 32); } } } diff --git a/tests/Tetrahedron_test.cpp b/tests/Tetrahedron_test.cpp index 26a8b7787..9fb99d654 100644 --- a/tests/Tetrahedron_test.cpp +++ b/tests/Tetrahedron_test.cpp @@ -20,7 +20,7 @@ using namespace cdt; using namespace std; using namespace foliated_triangulations; -static inline auto constexpr RADIUS_2 = 2.0 * std::numbers::inv_sqrt3_v; +static inline constexpr auto RADIUS_2 = 2.0 * std::numbers::inv_sqrt3_v; SCENARIO("Construct a tetrahedron in a Delaunay triangulation" * doctest::test_suite("tetrahedron")) @@ -90,8 +90,8 @@ SCENARIO("Find distances between points of the tetrahedron" * causal_vertices.emplace_back(v_4, 2); WHEN("The Foliated triangulation is constructed with these points.") { - FoliatedTriangulation triangulation(causal_vertices); - squared_distance constexpr r_2; + FoliatedTriangulation triangulation(causal_vertices); + constexpr squared_distance r_2; THEN("The triangulation is initialized correctly.") { REQUIRE(triangulation.is_initialized()); } THEN("The squared distances of vertices from origin are correct.") @@ -168,7 +168,7 @@ 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); + CHECK_EQ(expected_cell_type<3>(cell), CellType::THREE_ONE); } THEN("There is one (3,1) simplex.") diff --git a/tests/Torus_test.cpp b/tests/Torus_test.cpp index 8939a6bb2..821b6a82f 100644 --- a/tests/Torus_test.cpp +++ b/tests/Torus_test.cpp @@ -10,37 +10,71 @@ #include +#include +#include +#include +#include + #include "Torus_d.hpp" using namespace cdt::experimental::torus; +namespace +{ + void check_cube_points(std::vector const& points, + std::size_t const expected_size, + int const expected_dimension) + { + REQUIRE_EQ(points.size(), expected_size); + std::set> unique_points; + + for (std::size_t point_index = 0; point_index < points.size(); + ++point_index) + { + CAPTURE(point_index); + auto const& point = points[point_index]; + CHECK_EQ(point.dimension(), expected_dimension); + + std::vector coordinates; + coordinates.reserve(static_cast(expected_dimension)); + for (auto coordinate = point.cartesian_begin(); + coordinate != point.cartesian_end(); ++coordinate) + { + CHECK(std::isfinite(*coordinate)); + CHECK_GE(*coordinate, -1.0); + CHECK_LE(*coordinate, 1.0); + coordinates.push_back(*coordinate); + } + CHECK(unique_points.insert(std::move(coordinates)).second); + } + } +} // namespace + SCENARIO("Torus construction" * doctest::test_suite("torus")) { - std::size_t constexpr NUMBER_OF_POINTS = 250; - std::vector points; + constexpr std::size_t NUMBER_OF_POINTS = 250; + std::vector points; points.reserve(NUMBER_OF_POINTS); GIVEN("A 2-torus") { WHEN("A 2-torus is constructed.") { - THEN("It should not throw.") - { - int constexpr dim = 3; - REQUIRE_NOTHROW(make_d_cube(points, NUMBER_OF_POINTS, dim)); - CHECK_EQ(points.size(), NUMBER_OF_POINTS); - } + constexpr int dim = 3; + make_d_cube(points, NUMBER_OF_POINTS, dim); + + THEN("It contains distinct finite points in the requested cube.") + { check_cube_points(points, NUMBER_OF_POINTS, dim); } } } GIVEN("A 3-torus") { WHEN("A 3-torus is constructed.") { - THEN("It should not throw.") - { - int constexpr dim = 4; - REQUIRE_NOTHROW(make_d_cube(points, NUMBER_OF_POINTS, dim)); - CHECK_EQ(points.size(), NUMBER_OF_POINTS); - } + constexpr int dim = 4; + make_d_cube(points, NUMBER_OF_POINTS, dim); + + THEN("It contains distinct finite points in the requested cube.") + { check_cube_points(points, NUMBER_OF_POINTS, dim); } } } } diff --git a/tests/Utilities_test.cpp b/tests/Utilities_test.cpp index b3db0758b..b4ea2b82e 100644 --- a/tests/Utilities_test.cpp +++ b/tests/Utilities_test.cpp @@ -22,6 +22,7 @@ #include #include #include +#include using namespace cdt; using namespace std; @@ -172,9 +173,9 @@ SCENARIO("Various string/stream/time utilities" * doctest::test_suite("utilities")) { spdlog::debug("Various string/stream/time utilities.\n"); - GIVEN("A topology_type.") + GIVEN("A Topology.") { - auto constexpr this_topology = topology_type::SPHERICAL; + constexpr auto this_topology = Topology::SPHERICAL; WHEN("Operator<< is invoked.") { stringstream buffer; @@ -211,11 +212,11 @@ SCENARIO("Various string/stream/time utilities" * } WHEN("A filename is generated.") { - auto constexpr this_topology = topology_type::SPHERICAL; - auto constexpr dimensions = 3; - auto constexpr simplices = 6700; - auto constexpr timeslices = 16; - auto const filename = + constexpr auto this_topology = Topology::SPHERICAL; + constexpr auto dimensions = 3; + constexpr auto simplices = 6700; + constexpr auto timeslices = 16; + auto const filename = make_filename(this_topology, dimensions, simplices, timeslices, INITIAL_RADIUS, FOLIATION_SPACING); THEN("The output is correct.") @@ -270,7 +271,7 @@ SCENARIO("Reading and writing Delaunay triangulations to files" * 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); + auto const filename = make_filename(manifold, cdt::RandomSeed{92}, 7); THEN("The seed and completed pass are recorded before the OFF suffix") { CHECK_NE(filename.string().find("-seed-92-pass-7.off"), @@ -332,7 +333,7 @@ SCENARIO("Reading and writing Delaunay triangulations to files" * TemporaryDirectory const directory; auto const filename = directory.file("checkpoint.off"); auto metadata = make_reproducibility_metadata( - manifold, cdt::Random_seed{92}, Artifact_kind::CHECKPOINT); + manifold, cdt::RandomSeed{92}, ArtifactKind::CHECKPOINT); metadata.desired_simplices = 64; metadata.desired_timeslices = 3; metadata.alpha = 0.6L; @@ -341,6 +342,7 @@ SCENARIO("Reading and writing Delaunay triangulations to files" * metadata.configured_passes = 10; metadata.checkpoint_interval = 2; metadata.completed_passes = 4; + metadata.max_threads = 4; metadata.transition_trace = 0x1234; metadata.transition_count = 17; @@ -371,11 +373,12 @@ SCENARIO("Reading and writing Delaunay triangulations to files" * CHECK_NE(contents.find("desired.simplices=64"), std::string::npos); CHECK_NE(contents.find("alpha=0.6"), std::string::npos); CHECK_NE(contents.find("completed_passes=4"), std::string::npos); + CHECK_NE(contents.find("parallel.max_threads=4"), std::string::npos); CHECK_NE(contents.find("transition_trace.fnv1a64=0000000000001234"), std::string::npos); CHECK_NE(contents.find("placement.fnv1a64="), std::string::npos); CHECK_NE(contents.find("topology.fnv1a64="), std::string::npos); - CHECK_NOTHROW(read_file>(filename)); + CHECK_NOTHROW(static_cast(read_file>(filename))); auto payload_temporary = filename; payload_temporary += ".tmp"; auto metadata_temporary = sidecar; @@ -454,7 +457,7 @@ SCENARIO("File serialization reports complete failures" * { THEN("The trailing input is reported.") { - CHECK_THROWS_AS(read_file(filename), + CHECK_THROWS_AS(static_cast(read_file(filename)), std::filesystem::filesystem_error); } } @@ -472,7 +475,7 @@ SCENARIO("File serialization reports complete failures" * { THEN("The malformed input is reported.") { - CHECK_THROWS_AS(read_file(filename), + CHECK_THROWS_AS(static_cast(read_file(filename)), std::filesystem::filesystem_error); } } @@ -492,7 +495,7 @@ SCENARIO("File serialization reports complete failures" * { auto const filename = directory.file("truncated.off"); auto const metadata = make_reproducibility_metadata( - manifold, cdt::Random_seed{92}, Artifact_kind::CHECKPOINT); + manifold, cdt::RandomSeed{92}, ArtifactKind::CHECKPOINT); auto checkpoint_metadata = metadata; checkpoint_metadata.completed_passes = 2; write_file(filename, triangulation, checkpoint_metadata); @@ -502,7 +505,7 @@ SCENARIO("File serialization reports complete failures" * THEN("The checksum mismatch is diagnosed before topology is accepted.") { - CHECK_THROWS_AS(read_file>(filename), + CHECK_THROWS_AS(static_cast(read_file>(filename)), std::filesystem::filesystem_error); } } @@ -511,7 +514,7 @@ SCENARIO("File serialization reports complete failures" * { auto const filename = directory.file("malformed-metadata.off"); auto const metadata = make_reproducibility_metadata( - manifold, cdt::Random_seed{92}, Artifact_kind::CHECKPOINT); + manifold, cdt::RandomSeed{92}, ArtifactKind::CHECKPOINT); auto checkpoint_metadata = metadata; checkpoint_metadata.completed_passes = 2; write_file(filename, triangulation, checkpoint_metadata); @@ -522,7 +525,7 @@ SCENARIO("File serialization reports complete failures" * THEN("The artifact is rejected rather than treated as legacy data.") { - CHECK_THROWS_AS(read_file>(filename), + CHECK_THROWS_AS(static_cast(read_file>(filename)), std::filesystem::filesystem_error); } } @@ -531,7 +534,7 @@ SCENARIO("File serialization reports complete failures" * { auto const filename = directory.file("changed-topology-fingerprint.off"); auto metadata = make_reproducibility_metadata( - manifold, cdt::Random_seed{92}, Artifact_kind::CHECKPOINT); + manifold, cdt::RandomSeed{92}, ArtifactKind::CHECKPOINT); metadata.completed_passes = 2; write_file(filename, triangulation, metadata); corrupt_metadata_hex_field(metadata_filename(filename), @@ -539,7 +542,7 @@ SCENARIO("File serialization reports complete failures" * THEN("The semantic manifest/payload mismatch is rejected.") { - CHECK_THROWS_AS(read_file>(filename), + CHECK_THROWS_AS(static_cast(read_file>(filename)), std::filesystem::filesystem_error); } } @@ -548,7 +551,7 @@ SCENARIO("File serialization reports complete failures" * { auto const filename = directory.file("changed-incidence-count.off"); auto metadata = make_reproducibility_metadata( - manifold, cdt::Random_seed{92}, Artifact_kind::CHECKPOINT); + manifold, cdt::RandomSeed{92}, ArtifactKind::CHECKPOINT); metadata.completed_passes = 2; write_file(filename, triangulation, metadata); replace_metadata_field(metadata_filename(filename), "actual.simplices", @@ -556,7 +559,7 @@ SCENARIO("File serialization reports complete failures" * THEN("The semantic manifest/payload mismatch is rejected.") { - CHECK_THROWS_AS(read_file>(filename), + CHECK_THROWS_AS(static_cast(read_file>(filename)), std::filesystem::filesystem_error); } } @@ -565,7 +568,7 @@ SCENARIO("File serialization reports complete failures" * { auto const filename = directory.file("invalid-completed-passes.off"); auto metadata = make_reproducibility_metadata( - manifold, cdt::Random_seed{92}, Artifact_kind::CHECKPOINT); + manifold, cdt::RandomSeed{92}, ArtifactKind::CHECKPOINT); metadata.completed_passes = 2; write_file(filename, triangulation, metadata); replace_metadata_field(metadata_filename(filename), "completed_passes", @@ -573,16 +576,44 @@ SCENARIO("File serialization reports complete failures" * THEN("The malformed typed field is rejected.") { - CHECK_THROWS_AS(read_file>(filename), + CHECK_THROWS_AS(static_cast(read_file>(filename)), std::filesystem::filesystem_error); } } + WHEN("A configured thread limit is zero.") + { + auto const filename = directory.file("invalid-thread-limit.off"); + auto metadata = make_reproducibility_metadata( + manifold, cdt::RandomSeed{92}, ArtifactKind::CHECKPOINT); + metadata.completed_passes = 2; + metadata.max_threads = 1; + write_file(filename, triangulation, metadata); + replace_metadata_field(metadata_filename(filename), + "parallel.max_threads", "0"); + + THEN("The invalid resource provenance is rejected.") + { + try + { + static_cast(read_file>(filename)); + FAIL_CHECK("A zero thread limit was accepted."); + } + catch (std::filesystem::filesystem_error const& error) + { + CHECK_EQ(error.code(), + std::make_error_code(std::errc::illegal_byte_sequence)); + CHECK( + std::string_view{error.what()}.contains("invalid thread limit")); + } + } + } + WHEN("Caller-supplied payload-derived provenance is stale.") { auto const filename = directory.file("reconciled-provenance.off"); auto metadata = make_reproducibility_metadata( - manifold, cdt::Random_seed{92}, Artifact_kind::CHECKPOINT); + manifold, cdt::RandomSeed{92}, ArtifactKind::CHECKPOINT); metadata.completed_passes = 2; metadata.actual_simplices = 999; metadata.topology_fingerprint = 0; @@ -591,7 +622,7 @@ SCENARIO("File serialization reports complete failures" * THEN("The writer derives those fields from the serialized state.") { CHECK_NOTHROW(write_file(filename, triangulation, metadata)); - CHECK_NOTHROW(read_file>(filename)); + CHECK_NOTHROW(static_cast(read_file>(filename))); std::ifstream input{metadata_filename(filename)}; std::string const contents{std::istreambuf_iterator{input}, std::istreambuf_iterator{}}; @@ -612,7 +643,10 @@ SCENARIO("File serialization reports complete failures" * WHEN("The abstract triangulation is parsed.") { THEN("TDS integrity does not impose the Delaunay empty-sphere property.") - { CHECK_NOTHROW(read_file(filename)); } + { + CHECK_NOTHROW( + static_cast(read_file(filename))); + } } } } @@ -639,7 +673,7 @@ SCENARIO("Randomizing functions" * doctest::test_suite("utilities")) } GIVEN("A container of ints") { - Int_precision constexpr VECTOR_TEST_SIZE = 100; + constexpr Int_precision VECTOR_TEST_SIZE = 100; array container{}; iota(container.begin(), container.end(), 0); WHEN("The container is shuffled.") @@ -663,15 +697,15 @@ SCENARIO("Randomizing functions" * doctest::test_suite("utilities")) { cdt::Random generator{92}; CAPTURE(generator.seed()); - auto constexpr min = 64; - auto constexpr max = 6400; - 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}; + constexpr auto min = 64; + constexpr auto max = 6400; + 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.") { // All elements are >= min @@ -699,7 +733,7 @@ SCENARIO("Randomizing functions" * doctest::test_suite("utilities")) array container = {value1, value2, value3, value4, value5, value6}; THEN("They should all fall within the range.") { - auto constexpr min = 1; + constexpr auto min = 1; // All elements are >= min CHECK_GE(*ranges::min_element(container), min); @@ -714,9 +748,9 @@ SCENARIO("Randomizing functions" * doctest::test_suite("utilities")) { cdt::Random generator{92}; CAPTURE(generator.seed()); - auto constexpr min = 0.0L; - auto constexpr max = 1.0L; - auto const value = generate_random_real(generator, min, max); + constexpr auto min = 0.0L; + constexpr auto max = 1.0L; + auto const value = generate_random_real(generator, min, max); THEN("The real number should lie within that range.") { REQUIRE_LE(min, value); @@ -781,8 +815,9 @@ SCENARIO("Expected points per timeslice" * doctest::test_suite("utilities")) { THEN("A std::invalid_argument exception is thrown.") { - REQUIRE_THROWS_AS(expected_points_per_timeslice(4, 640000, 64), - std::invalid_argument); + REQUIRE_THROWS_AS( + static_cast(expected_points_per_timeslice(4, 640000, 64)), + std::invalid_argument); } } } @@ -796,7 +831,7 @@ SCENARIO("Exact number (Gmpzf) conversion" * doctest::test_suite("utilities")) Gmpzf const TEST_VALUE = 0.17; WHEN("We convert it to double.") { - auto const converted_value = Gmpzf_to_double(TEST_VALUE); + auto const converted_value = gmpzf_to_double(TEST_VALUE); THEN("It should be exact when converted back from double to Gmpzf.") { REQUIRE_EQ(TEST_VALUE, Gmpzf(converted_value)); } } diff --git a/tests/Vertex_test.cpp b/tests/Vertex_test.cpp index e4cadba73..a300fdaaf 100644 --- a/tests/Vertex_test.cpp +++ b/tests/Vertex_test.cpp @@ -20,7 +20,7 @@ using namespace cdt; using namespace manifolds; -static inline auto constexpr RADIUS_2 = 2.0 * std::numbers::inv_sqrt3_v; +static inline constexpr auto RADIUS_2 = 2.0 * std::numbers::inv_sqrt3_v; namespace { diff --git a/tests/semgrep/functional_boundaries.cpp b/tests/semgrep/functional_boundaries.cpp index 9dd09609d..489c6417f 100644 --- a/tests/semgrep/functional_boundaries.cpp +++ b/tests/semgrep/functional_boundaries.cpp @@ -50,9 +50,9 @@ class Imperative_move_run void consume(Command& command) { // ruleid: cdt.cpp.move-run-accounting-is-value-oriented - m_attempted_moves += command.get_attempted(); + m_attempted_moves += command.attempted(); // ruleid: cdt.cpp.move-run-accounting-is-value-oriented - m_failed_moves += command.get_failed(); + m_failed_moves += command.failed(); } private: @@ -78,7 +78,7 @@ auto collect_command_result(Command& command) { Move_counter attempted_moves{}; // ok: cdt.cpp.move-run-accounting-is-value-oriented - attempted_moves += command.get_attempted(); + attempted_moves += command.attempted(); return MoveCommandResults{std::move(attempted_moves), {}, {}}; } From 6ef7dd62c1d1a71bb834611a4a89a15a47f7de57 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Fri, 24 Jul 2026 17:36:26 -0700 Subject: [PATCH 2/5] fix(concurrency): harden parallel build and provenance - Correct pkgx tool-list handling across CI and coverage workflows. - Refresh source revision metadata when the Git state changes. - Preserve optional thread limits when parsing persistence metadata. - Enforce move-table consistency and avoid duplicate cell classification. --- .github/CONTRIBUTING.md | 5 +-- .github/workflows/ci.yml | 4 +-- .github/workflows/codecov-upload.yml | 2 +- CMakeLists.txt | 7 +++++ CMakePresets.json | 1 - include/Foliated_triangulation.hpp | 13 ++++---- include/Move_tracker.hpp | 1 + include/Utilities.hpp | 47 ++++++++++++++++------------ tests/Utilities_test.cpp | 22 +++++++++++++ 9 files changed, 70 insertions(+), 32 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 695a8bf03..6591ca65e 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -54,8 +54,9 @@ substantial implementation so its maintenance value and scope can be agreed upon CGAL/oneTBB configuration and runs the same scientific suite plus the replayable parallel stress launcher, for 25 entries. 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. + GitHub Actions runs both `just ci` and `just build-parallel` in its Ubuntu GCC, Ubuntu Clang, macOS AppleClang, + and Windows MSVC jobs. The Windows job continues to compile with native MSVC; LLVM tooling is used only for + source formatting. 6. Run the relevant Linux sanitizer configuration for changes involving memory, lifetime, undefined behavior, or concurrency: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15f354f61..61d516522 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,7 @@ jobs: if: runner.os != 'Windows' uses: pkgxdev/setup@4d4ae97af87ccb39ab8be4e073dea697fef2c6f7 # v5.0.0 with: - +: | + +: >- llvm.org@${{ steps.tool-versions.outputs.llvm }} cmake.org@${{ steps.tool-versions.outputs.cmake }} ninja-build.org@${{ steps.tool-versions.outputs.ninja }} @@ -96,7 +96,7 @@ jobs: if: runner.os == 'Windows' uses: pkgxdev/setup@4d4ae97af87ccb39ab8be4e073dea697fef2c6f7 # v5.0.0 with: - +: | + +: >- cmake.org@${{ steps.tool-versions.outputs.cmake }} ninja-build.org@${{ steps.tool-versions.outputs.ninja }} diff --git a/.github/workflows/codecov-upload.yml b/.github/workflows/codecov-upload.yml index dc34f8aa2..fdef9b81c 100644 --- a/.github/workflows/codecov-upload.yml +++ b/.github/workflows/codecov-upload.yml @@ -65,7 +65,7 @@ jobs: - name: Set up coverage toolchain uses: pkgxdev/setup@4d4ae97af87ccb39ab8be4e073dea697fef2c6f7 # v5.0.0 with: - +: | + +: >- gnu.org/gcc@16 cmake.org@${{ steps.tool-versions.outputs.cmake }} ninja-build.org@${{ steps.tool-versions.outputs.ninja }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ad5a8220..a02f1e893 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,13 @@ set(CDT_VERSION "${PROJECT_VERSION}${CDT_VERSION_SUFFIX}") set(CDT_SOURCE_REVISION "unknown") find_package(Git QUIET) if(GIT_FOUND) + if(IS_DIRECTORY "${PROJECT_SOURCE_DIR}/.git") + set_property( + DIRECTORY APPEND + PROPERTY CMAKE_CONFIGURE_DEPENDS + "${PROJECT_SOURCE_DIR}/.git/HEAD" + "${PROJECT_SOURCE_DIR}/.git/index") + endif() execute_process( COMMAND "${GIT_EXECUTABLE}" rev-parse HEAD WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" diff --git a/CMakePresets.json b/CMakePresets.json index bab114249..5d32a5c96 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -47,7 +47,6 @@ "rhs": "Linux" }, "cacheVariables": { - "CMAKE_C_COMPILER": "clang", "CMAKE_CXX_COMPILER": "clang++", "CMAKE_BUILD_TYPE": "RelWithDebInfo", "CMAKE_CXX_FLAGS": "-Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Wcast-qual -Wformat=2 -Werror=float-equal -Wshadow -Wcast-align -Wunused -Wnull-dereference -Wdouble-promotion -Wimplicit-fallthrough -Wextra-semi -Woverloaded-virtual -Wnon-virtual-dtor -Wold-style-cast", diff --git a/include/Foliated_triangulation.hpp b/include/Foliated_triangulation.hpp index 61837c300..7fbea5be9 100644 --- a/include/Foliated_triangulation.hpp +++ b/include/Foliated_triangulation.hpp @@ -1052,12 +1052,13 @@ namespace cdt::foliated_triangulations { auto const& cells = collect_cells(t_triangulation); std::vector> invalid_cells; - std::copy_if( - cells.begin(), cells.end(), std::back_inserter(invalid_cells), - [](auto const& cell) { - return expected_cell_type(cell) == CellType::ACAUSAL || - expected_cell_type(cell) == CellType::UNCLASSIFIED; - }); + std::copy_if(cells.begin(), cells.end(), std::back_inserter(invalid_cells), + [](auto const& cell) { + auto const classification = + expected_cell_type(cell); + return classification == CellType::ACAUSAL || + classification == CellType::UNCLASSIFIED; + }); return invalid_cells; } // find_invalid_timevalue_cells diff --git a/include/Move_tracker.hpp b/include/Move_tracker.hpp index 7770fbb8b..dd33a4ac9 100644 --- a/include/Move_tracker.hpp +++ b/include/Move_tracker.hpp @@ -63,6 +63,7 @@ namespace cdt::move_tracker using enum MoveType; constexpr std::array moves{TWO_THREE, THREE_TWO, TWO_SIX, SIX_TWO, FOUR_FOUR}; + static_assert(moves.size() == NUMBER_OF_3D_MOVES); if (move_choice >= moves.size()) { return std::nullopt; } return moves[move_choice]; } // move_from_index diff --git a/include/Utilities.hpp b/include/Utilities.hpp index ba570f854..25d3a2574 100644 --- a/include/Utilities.hpp +++ b/include/Utilities.hpp @@ -637,21 +637,22 @@ namespace cdt::utilities struct Parsed_persistence_metadata { - Payload_integrity payload; - ArtifactKind artifact; - cdt::RandomSeed seed; - cdt::RandomStream initialization_stream; - cdt::RandomStream transition_stream; - Topology topology; - Int_precision dimension; - Int_precision actual_vertices; - Int_precision actual_edges; - Int_precision actual_faces; - Int_precision actual_simplices; - Int_precision minimum_timeslice; - Int_precision maximum_timeslice; - std::uint64_t placement_fingerprint; - std::uint64_t topology_fingerprint; + Payload_integrity payload; + ArtifactKind artifact; + cdt::RandomSeed seed; + cdt::RandomStream initialization_stream; + cdt::RandomStream transition_stream; + Topology topology; + Int_precision dimension; + Int_precision actual_vertices; + Int_precision actual_edges; + Int_precision actual_faces; + Int_precision actual_simplices; + Int_precision minimum_timeslice; + Int_precision maximum_timeslice; + std::optional max_threads; + std::uint64_t placement_fingerprint; + std::uint64_t topology_fingerprint; }; [[nodiscard]] inline auto read_persistence_metadata( @@ -890,12 +891,17 @@ namespace cdt::utilities "Persistence metadata contains invalid completed passes", path, std::make_error_code(std::errc::illegal_byte_sequence)); } - if (values.contains("parallel.max_threads") && - parse_unsigned(values.at("parallel.max_threads"), 10, path) == 0) + std::optional max_threads; + if (auto const field = values.find("parallel.max_threads"); + field != values.end()) { - throw std::filesystem::filesystem_error( - "Persistence metadata contains an invalid thread limit", path, - std::make_error_code(std::errc::illegal_byte_sequence)); + max_threads = parse_unsigned(field->second, 10, path); + if (*max_threads == 0) + { + throw std::filesystem::filesystem_error( + "Persistence metadata contains an invalid thread limit", path, + std::make_error_code(std::errc::illegal_byte_sequence)); + } } auto const transition_field_count = @@ -933,6 +939,7 @@ namespace cdt::utilities .actual_simplices = actual_simplices, .minimum_timeslice = minimum_timeslice, .maximum_timeslice = maximum_timeslice, + .max_threads = max_threads, .placement_fingerprint = parse_unsigned(values.at("placement.fnv1a64"), 16, path), .topology_fingerprint = diff --git a/tests/Utilities_test.cpp b/tests/Utilities_test.cpp index b4ea2b82e..c58e98f79 100644 --- a/tests/Utilities_test.cpp +++ b/tests/Utilities_test.cpp @@ -378,6 +378,10 @@ SCENARIO("Reading and writing Delaunay triangulations to files" * std::string::npos); CHECK_NE(contents.find("placement.fnv1a64="), std::string::npos); CHECK_NE(contents.find("topology.fnv1a64="), std::string::npos); + auto const parsed_metadata = + utilities::detail::read_persistence_metadata(sidecar); + REQUIRE(parsed_metadata.max_threads.has_value()); + CHECK_EQ(*parsed_metadata.max_threads, 4); CHECK_NOTHROW(static_cast(read_file>(filename))); auto payload_temporary = filename; payload_temporary += ".tmp"; @@ -387,6 +391,24 @@ SCENARIO("Reading and writing Delaunay triangulations to files" * CHECK_FALSE(std::filesystem::exists(metadata_temporary)); } } + WHEN("A stochastic artifact omits the optional thread limit") + { + TemporaryDirectory const directory; + auto const filename = directory.file("checkpoint.off"); + auto metadata = make_reproducibility_metadata( + manifold, cdt::RandomSeed{92}, ArtifactKind::CHECKPOINT); + metadata.completed_passes = 4; + + write_file(filename, manifold.delaunay_snapshot(), metadata); + + THEN("The parsed provenance preserves the field's absence") + { + auto const parsed_metadata = + utilities::detail::read_persistence_metadata( + metadata_filename(filename)); + CHECK_FALSE(parsed_metadata.max_threads.has_value()); + } + } } } From f01fca697d36f7c9a49c5665fb4b7adee78e868d Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Fri, 24 Jul 2026 19:19:12 -0700 Subject: [PATCH 3/5] build(tooling): enforce pkgx-first pinned toolchains Keep Unix CI on pkgx and restrict the CMake and Ninja wheel fallback to Windows, where the pkgx packages are unavailable. - Install locked Python tooling exclusively from wheels - Use pinned upstream actionlint and zizmor distributions - Enforce the toolchain policy with a repository Semgrep rule - Refresh development-tool and GitHub Action pins --- .github/CONTRIBUTING.md | 4 +- .github/actions/setup-just/action.yml | 2 +- .github/workflows/ci.yml | 28 ++--- Justfile | 132 ++++++++++++--------- README.md | 18 +-- pyproject.toml | 20 +++- scripts/mnist_experiment.py | 2 +- scripts/optimize_initialize.py | 2 +- semgrep.yaml | 24 ++++ tests/semgrep/tooling_policy.yml | 28 +++++ uv.lock | 164 ++++++++++++-------------- 11 files changed, 250 insertions(+), 174 deletions(-) create mode 100644 tests/semgrep/tooling_policy.yml diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 6591ca65e..66422613a 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -56,7 +56,9 @@ substantial implementation so its maintenance value and scope can be agreed upon `just clang-tidy` with the pinned LLVM 22 toolchain and review its advisory diagnostics. GitHub Actions runs both `just ci` and `just build-parallel` 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. + source formatting. Toolchain setup is pkgx-first; because pkgx does not currently publish its CMake and Ninja + packages for Windows, that job uses the exact Justfile pins available as PyPI wheels through + `uv tool install --no-build`. 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 1a3136ade..8d618707f 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/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + uses: taiki-e/install-action@7572810d7dd469b651bb7793945692cf78da5dd7 # v2.85.0 with: tool: just@${{ steps.resolve.outputs.version }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61d516522..6395d8565 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,8 +79,8 @@ jobs: echo "cmake=$(just --evaluate cmake_version)" echo "llvm=$(just --evaluate llvm_version)" echo "ninja=$(just --evaluate ninja_version)" + echo "ninja-windows-wheel=$(just --evaluate ninja_windows_wheel_version)" echo "uv=$(just --evaluate uv_version)" - echo "zizmor=$(just --evaluate zizmor_version)" } >> "$GITHUB_OUTPUT" - name: Set up canonical CI environment with pkgx @@ -92,14 +92,6 @@ jobs: cmake.org@${{ steps.tool-versions.outputs.cmake }} ninja-build.org@${{ steps.tool-versions.outputs.ninja }} - - name: Set up pinned CMake and Ninja on Windows - if: runner.os == 'Windows' - uses: pkgxdev/setup@4d4ae97af87ccb39ab8be4e073dea697fef2c6f7 # v5.0.0 - with: - +: >- - cmake.org@${{ steps.tool-versions.outputs.cmake }} - ninja-build.org@${{ steps.tool-versions.outputs.ninja }} - - name: Set up Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: @@ -110,13 +102,17 @@ jobs: with: version: ${{ steps.tool-versions.outputs.uv }} - - name: Synchronize Python development dependencies - run: uv sync --locked --group dev - - - name: Install zizmor - uses: taiki-e/install-action@07b4745e0c39a41822af610387492e3e53aa222b # v2.83.4 - with: - tool: zizmor@${{ steps.tool-versions.outputs.zizmor }} + # pkgx's CMake and Ninja packages do not currently support Windows. + - name: Set up pinned Windows wheel toolchain + if: runner.os == 'Windows' + shell: pwsh + env: + CMAKE_VERSION: ${{ steps.tool-versions.outputs.cmake }} + NINJA_VERSION: ${{ steps.tool-versions.outputs.ninja-windows-wheel }} + run: | + uv tool install --no-build "cmake==$env:CMAKE_VERSION" + uv tool install --no-build "ninja==$env:NINJA_VERSION" + uv tool dir --bin | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - name: Install pinact on Windows if: runner.os == 'Windows' diff --git a/Justfile b/Justfile index be911146e..3dc09cfbf 100644 --- a/Justfile +++ b/Justfile @@ -5,17 +5,19 @@ set minimum-version := "1.57.0" set shell := ["bash", "-euo", "pipefail", "-c"] just_version := "1.57.0" -uv_version := "0.11.31" +uv_version := "0.11.32" git_cliff_version := "2.13.1" +actionlint_version := "1.7.12" pinact_version := "4.1.0" pinact_module := "github.com/suzuki-shunsuke/pinact/v4/cmd/pinact@v" + pinact_version llvm_version := "22" cmake_minimum_version := "4.4.0" cmake_version := "4.4.0" ninja_version := "1.13.2" +ninja_windows_wheel_version := "1.13.0" doxygen_version := "1.17.0" graphviz_version := "15.1.0" -zizmor_version := "1.26.1" +zizmor_version := "1.28.0" 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" } cgal_benchmark_binary := if os_family() == "windows" { "out/build/reference/tests/CDT_cgal_benchmark.exe" } else { "out/build/reference/tests/CDT_cgal_benchmark" } @@ -132,39 +134,39 @@ clang-tidy: # Validate release metadata, citation fields, and version synchronization. [group('workflows')] -release-check: _ensure-uv - uv run --locked python scripts/release_check.py +release-check: _sync-python-dev + uv run --no-sync python scripts/release_check.py # Generate the changelog as though the requested release tag already exists. [group('release')] -changelog-unreleased version: _ensure-git-cliff _ensure-uv - uv run --locked python scripts/generate_changelog.py {{ quote(version) }} +changelog-unreleased version: _ensure-git-cliff _sync-python-dev + uv run --no-sync python scripts/generate_changelog.py {{ quote(version) }} # Validate and preview an annotated release tag without creating it. [group('release')] -tag-check version: _ensure-uv - uv run --locked cdt-tag-release {{ quote(version) }} --dry-run +tag-check version: _sync-python-dev + uv run --no-sync cdt-tag-release {{ quote(version) }} --dry-run # Create an annotated release tag from the matching CHANGELOG.md section. [group('release')] -tag version: _ensure-uv - uv run --locked cdt-tag-release {{ quote(version) }} +tag version: _sync-python-dev + uv run --no-sync cdt-tag-release {{ quote(version) }} # Scan production and correctness-test sources for repository-owned policies. [group('workflows')] -semgrep: _ensure-uv +semgrep: _sync-python-dev #!/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 + uv run --no-sync semgrep scan --error --strict --timeout 120 --no-git-ignore \ + --config semgrep.yaml --exclude tests/semgrep .github include src tests # Test repository-owned Semgrep rules against annotated positive and negative fixtures. [group('workflows')] -semgrep-test: _ensure-uv +semgrep-test: _sync-python-dev #!/usr/bin/env bash set -euo pipefail config_dir="$(mktemp -d "${TMPDIR:-/tmp}/cdt-semgrep-config.XXXXXX")" @@ -179,10 +181,10 @@ semgrep-test: _ensure-uv 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" + uv run --no-sync 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" + uv run --no-sync 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. @@ -206,47 +208,47 @@ python-check: python-format-check python-lint python-typecheck python-support-te # Apply Ruff lint fixes and formatting to Python source. [group('workflows')] -python-fix: _ensure-uv - uv run --locked ruff check scripts/ --fix - uv run --locked ruff format scripts/ +python-fix: _sync-python-dev + uv run --no-sync ruff check scripts/ --fix + uv run --no-sync ruff format scripts/ # Check Python formatting with Ruff. [group('workflows')] -python-format-check: _ensure-uv - uv run --locked ruff format --check scripts/ +python-format-check: _sync-python-dev + uv run --no-sync ruff format --check scripts/ # Lint Python source with Ruff. [group('workflows')] -python-lint: _ensure-uv - uv run --locked ruff check scripts/ +python-lint: _sync-python-dev + uv run --no-sync 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' +python-support-test: _sync-python-dev + uv run --no-sync 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 - uv run --locked cdt-tag-release --help >/dev/null +python-entrypoint-test: _sync-python-dev + uv run --no-sync cdt-bootstrap-vcpkg --help >/dev/null + uv run --no-sync cdt-optimize-initialize --help >/dev/null + uv run --no-sync cdt-mnist-experiment --help >/dev/null + uv run --no-sync cdt-tag-release --help >/dev/null # Synchronize the lightweight Python development environment from the lockfile. [group('workflows')] -python-sync: _ensure-uv - uv sync --locked --group dev +python-sync: _sync-python-dev + @echo "Python development environment synchronized." # Synchronize dependencies required by the optional experiment scripts. [group('workflows')] -python-sync-experiments: _ensure-uv - uv sync --locked --group dev --group experiments +python-sync-experiments: _sync-python-experiments + @echo "Python experiment environment synchronized." # Type-check Python support code with ty. [group('workflows')] -python-typecheck: _ensure-uv - uv run --locked ty check scripts/*.py scripts/tests/*.py --error all +python-typecheck: _sync-python-dev + uv run --no-sync ty check scripts/*.py scripts/tests/*.py --error all # Build as needed and run the primary CDT++ executable. [group('workflows')] @@ -267,14 +269,29 @@ default: @just --list [private] -_action-lint: _ensure-uv +_action-lint: #!/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') - uv run --locked actionlint "${files[@]}" + if command -v actionlint >/dev/null; then + actual_version="$(actionlint --version | head -n 1)" + if [[ "$actual_version" != "{{ actionlint_version }}" ]]; then + echo "actionlint {{ actionlint_version }} is required; found $actual_version." >&2 + exit 1 + fi + exec actionlint "${files[@]}" + fi + if command -v pkgx >/dev/null; then + exec pkgx "actionlint@{{ actionlint_version }}" "${files[@]}" + fi + if command -v go >/dev/null; then + exec go run "github.com/rhysd/actionlint/cmd/actionlint@v{{ actionlint_version }}" "${files[@]}" + fi + echo "actionlint {{ actionlint_version }} is required; install it, Go, or pkgx." >&2 + exit 1 [private] _build-unix: @@ -355,29 +372,39 @@ _ensure-uv: fi [private] -_format-check: _ensure-uv +_sync-python-dev: _ensure-uv + uv sync --locked --no-build --no-install-project --group dev + uv sync --locked --only-install-project --inexact --group dev + +[private] +_sync-python-experiments: _ensure-uv + uv sync --locked --no-build --no-install-project --group dev --group experiments + uv sync --locked --only-install-project --inexact --group dev --group experiments + +[private] +_format-check: _sync-python-dev #!/usr/bin/env bash set -euo pipefail - uv run --locked clang-format --version | grep -Eq 'clang-format version {{ llvm_version }}([.]|$)' + uv run --no-sync 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 - uv run --locked clang-format --dry-run --Werror "${files[@]}" + uv run --no-sync clang-format --dry-run --Werror "${files[@]}" fi [private] -_format-fix: _ensure-uv +_format-fix: _sync-python-dev #!/usr/bin/env bash set -euo pipefail - uv run --locked clang-format --version | grep -Eq 'clang-format version {{ llvm_version }}([.]|$)' + uv run --no-sync 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 - uv run --locked clang-format -i "${files[@]}" + uv run --no-sync clang-format -i "${files[@]}" fi [private] @@ -422,24 +449,15 @@ _whitespace-check: [[ "$status" -eq 1 ]] || exit "$status" [private] -_yaml-check: _ensure-uv +_yaml-check: _sync-python-dev #!/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') - uv run --locked yamllint "${files[@]}" + uv run --no-sync yamllint "${files[@]}" [private] -_zizmor: - #!/usr/bin/env bash - set -euo pipefail - if command -v zizmor >/dev/null; then - zizmor .github - elif command -v pkgx >/dev/null; then - pkgx zizmor .github - else - echo "zizmor is required; install it or install pkgx." >&2 - exit 1 - fi +_zizmor: _ensure-uv + uvx --no-build --from "zizmor=={{ zizmor_version }}" zizmor .github diff --git a/README.md b/README.md index 1427f7560..535b8d3b3 100644 --- a/README.md +++ b/README.md @@ -245,18 +245,22 @@ let pkgx supply the Unix environment ephemerally; pkgx remains optional. For example: ```bash -uv sync --locked --group dev -pkgx +just.systems@1.57.0 +git-scm.org +cmake.org@4.4.0 +ninja-build.org +python.org +zizmor just check +just python-sync +pkgx +just.systems@1.57.0 +git-scm.org +cmake.org@4.4.0 +ninja-build.org +python.org just check ``` All configure paths require CMake 4.4.0 or newer. The Justfile owns the tested 4.4.0 toolchain pin: pkgx-backed recipes remain reproducible at that version, -while direct configure paths accept newer compatible CMake releases. +while direct configure paths accept newer compatible CMake releases. CI is +pkgx-first; because pkgx does not currently publish its CMake and Ninja packages +for Windows, that job uses the exact Justfile pins available as PyPI wheels +through `uv tool install --no-build`. [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, -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. +Go, or a pkgx-provided Go fallback, then requires `yamllint`, `actionlint`, and `zizmor` to pass. Direct third-party +Python dependencies install only from locked wheels. The uv environment provides `clang-format` and `yamllint`; +actionlint uses its pinned upstream version, and zizmor uses its pinned PyPI wheel through `uvx`. ### vcpkg maintenance @@ -565,8 +569,8 @@ Synchronize them from the same uv lockfile when working on the experiment script ```bash just python-sync-experiments -uv run --locked --group experiments cdt-optimize-initialize -uv run --locked --group experiments cdt-mnist-experiment +uv run --no-sync cdt-optimize-initialize +uv run --no-sync cdt-mnist-experiment ``` Run these commands from the repository root. Set `COMET_API_KEY` before starting the parameter optimization; use diff --git a/pyproject.toml b/pyproject.toml index 85b640861..9e3e880e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,19 @@ build-backend = "uv_build" [tool.uv] default-groups = ["dev"] +no-build-package = [ + "clang-format", + "comet-ml", + "matplotlib", + "numpy", + "pyyaml", + "ruff", + "semgrep", + "tensorflow", + "ty", + "uv-build", + "yamllint", +] package = true [tool.uv.build-backend] @@ -34,12 +47,11 @@ source-exclude = [ [dependency-groups] 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", + "ruff==0.16.0", + "semgrep==1.171.0", + "ty==0.0.63", "yamllint==1.38.0", ] experiments = [ diff --git a/scripts/mnist_experiment.py b/scripts/mnist_experiment.py index 9a66f1804..191f288f5 100644 --- a/scripts/mnist_experiment.py +++ b/scripts/mnist_experiment.py @@ -53,7 +53,7 @@ def main(argv: Sequence[str] | None = None) -> int: _run_experiment() except ModuleNotFoundError as error: print( - f"Missing experiment dependency {error.name!r}; run with `uv run --group experiments cdt-mnist-experiment`.", + f"Missing experiment dependency {error.name!r}; run `just python-sync-experiments`, then retry with `uv run --no-sync cdt-mnist-experiment`.", file=sys.stderr, ) return 2 diff --git a/scripts/optimize_initialize.py b/scripts/optimize_initialize.py index 5c4ff2238..e35581a6f 100644 --- a/scripts/optimize_initialize.py +++ b/scripts/optimize_initialize.py @@ -246,7 +246,7 @@ def main(argv: Sequence[str] | None = None) -> int: _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`.", + f"Missing experiment dependency {error.name!r}; run `just python-sync-experiments`, then retry with `uv run --no-sync cdt-optimize-initialize`.", file=sys.stderr, ) return 2 diff --git a/semgrep.yaml b/semgrep.yaml index 773f9e571..45598b758 100644 --- a/semgrep.yaml +++ b/semgrep.yaml @@ -1,6 +1,30 @@ --- # Repository-owned Semgrep rules for narrow CDT++ maintenance invariants. rules: + - id: cdt.tooling.pkgx-first-toolchain + languages: + - generic + severity: ERROR + message: >- + Keep CMake and Ninja setup pkgx-first. Use pkgxdev/setup with versions + resolved from the Justfile; the only approved fallback is the documented + Windows-only pinned wheel installation through uv with --no-build. + metadata: + category: build + rationale: >- + A single pkgx-owned toolchain path prevents workflow-specific installers + and system package managers from drifting away from repository pins. + paths: + include: + - "/.github/**/*.yaml" + - "/.github/**/*.yml" + - "/tests/semgrep/**/*.yaml" + - "/tests/semgrep/**/*.yml" + pattern-either: + - pattern-regex: |- + (?mi)^[\t ]*(?:-[\t ]*)?uses:[\t ]*(?:lukka/get-cmake|jwlawson/actions-setup-cmake|aminya/setup-cpp)@ + - pattern-regex: |- + (?mi)^[^\r\n]*(?:apt-get|brew|choco|winget|pipx?)[\t ]+install[^\r\n]*\b(?:cmake|ninja)\b - id: cdt.cpp.no-crtp-inheritance languages: - cpp diff --git a/tests/semgrep/tooling_policy.yml b/tests/semgrep/tooling_policy.yml new file mode 100644 index 000000000..d5b5d9b5c --- /dev/null +++ b/tests/semgrep/tooling_policy.yml @@ -0,0 +1,28 @@ +--- +name: Toolchain policy fixtures +jobs: + direct-action: + runs-on: ubuntu-latest + steps: + # ruleid: cdt.tooling.pkgx-first-toolchain + - uses: lukka/get-cmake@0123456789abcdef + + direct-package-manager: + runs-on: ubuntu-latest + steps: + # ruleid: cdt.tooling.pkgx-first-toolchain + - run: brew install cmake ninja + + pkgx-first: + runs-on: ubuntu-latest + steps: + # ok: cdt.tooling.pkgx-first-toolchain + - uses: pkgxdev/setup@0123456789abcdef + with: + +: cmake.org@4.4.0 ninja-build.org@1.13.2 + + documented-windows-fallback: + runs-on: windows-latest + steps: + # ok: cdt.tooling.pkgx-first-toolchain + - run: uv tool install --no-build "cmake==4.4.0" diff --git a/uv.lock b/uv.lock index 2379aabd2..55859806d 100644 --- a/uv.lock +++ b/uv.lock @@ -11,19 +11,13 @@ 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" +version = "0.8.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" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, ] [[package]] @@ -86,7 +80,6 @@ source = { editable = "." } [package.dev-dependencies] dev = [ - { name = "actionlint-py" }, { name = "clang-format" }, { name = "pyyaml" }, { name = "ruff" }, @@ -105,12 +98,11 @@ 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 = "ruff", specifier = "==0.16.0" }, + { name = "semgrep", specifier = "==1.171.0" }, + { name = "ty", specifier = "==0.0.63" }, { name = "yamllint", specifier = "==1.38.0" }, ] experiments = [ @@ -122,11 +114,11 @@ experiments = [ [[package]] name = "certifi" -version = "2026.6.17" +version = "2026.7.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, ] [[package]] @@ -201,14 +193,14 @@ wheels = [ [[package]] name = "click" -version = "8.1.8" +version = "8.4.2" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, ] [[package]] @@ -462,23 +454,23 @@ wheels = [ [[package]] name = "grpcio" -version = "1.82.1" +version = "1.83.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/90/bc/656b89387d6f4ed7e0686c7b64c2ae7e554a759aa58122c8e5fb99392c32/grpcio-1.82.1.tar.gz", hash = "sha256:707b24abd90fcb1e45bcc080577da1dbf9971d107490589b9539af8e1e77b4b5", size = 13187300, upload-time = "2026-07-08T12:36:16.588Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/98/304898ac4e04e2d5e4e4c2eadc178b1f2a16d5f4bc2f91306c87d64680b9/grpcio-1.83.0.tar.gz", hash = "sha256:7674587248fbbb2ac6e4eecf83a8a0f3d91a928f941de571acfd3a2f007fbc24", size = 13428824, upload-time = "2026-07-23T15:20:37.759Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/88/d1350bf3343a2ed87d801584e40609f6c6bd3087926eeca03de50348cf4a/grpcio-1.82.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:c09bd5fa0d5b1fbd773ec349fe61441c3e4ebf168c229aa7538a820bdfad6a58", size = 6144689, upload-time = "2026-07-08T12:34:55.567Z" }, - { url = "https://files.pythonhosted.org/packages/e6/33/71875cdecd27c24ac1385d4783a09853f01b84a825a36aec2a2bc7d0d080/grpcio-1.82.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:1eae24810720734598e3e6a1a528d5de0f265fe3fc86575e9ecce424b9ec7379", size = 11952034, upload-time = "2026-07-08T12:34:58.128Z" }, - { url = "https://files.pythonhosted.org/packages/82/b2/d9125df3d8a140dec12cc82c05b7deafedeababcff6496f28b2fd5634d10/grpcio-1.82.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a6bd5daf5bde7b24d7ad2cbaf8bf9eac620d96222016bb5e7ddde930dec0673f", size = 6710772, upload-time = "2026-07-08T12:35:01.33Z" }, - { url = "https://files.pythonhosted.org/packages/88/9b/69e2d1627398b964f34437dc476a5aff5a2cc8e7f247d26272b5674b5faf/grpcio-1.82.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1ecfde669cb687ac020d31ff76debe5dc7a62213335f02262eb6625628da1c03", size = 7450677, upload-time = "2026-07-08T12:35:03.926Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e7/8f855ca29c294956122a2a73023655b9b02602d5111dad2b9b00e7631c68/grpcio-1.82.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:011c8badee95734dee8bf05ce3464756a0ac3ebb8d443afd20c0e2b5e4640ad9", size = 6886855, upload-time = "2026-07-08T12:35:06.174Z" }, - { url = "https://files.pythonhosted.org/packages/5f/f0/fa87e85f49925f44c479d07e58b051e69bcfef6b6d5fbc6749d140f6730a/grpcio-1.82.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b85f4564926fb23114d239392bdcae200db1e6179629edd7d7ab0ab89c96a197", size = 7501323, upload-time = "2026-07-08T12:35:08.49Z" }, - { url = "https://files.pythonhosted.org/packages/67/55/2e0b10ae1d3ef9dcc480b91dc2158f4931fc4675d3af0a2836e39b2a744f/grpcio-1.82.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2c0c8270833395644c3fe6b6a806397955a2bc0538000a19a78b90c05a6c16e0", size = 8536899, upload-time = "2026-07-08T12:35:10.975Z" }, - { url = "https://files.pythonhosted.org/packages/bd/95/a3d8b0431fa221efc51ee39d73595ede74ba82a43b7c4313192e580face2/grpcio-1.82.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2ba199205ff46c7778290fe1673c91ac8e7e45678dd5c86e9e56fa33ec8788f6", size = 7913892, upload-time = "2026-07-08T12:35:13.944Z" }, - { url = "https://files.pythonhosted.org/packages/b8/92/f2651ec704d9852a56faef394775038afba435b50ce82ab2404d119c3355/grpcio-1.82.1-cp312-cp312-win32.whl", hash = "sha256:06127691866e295c14e84a1fb86356dd962254f6abd0da4ca4b001eea9e89438", size = 4240985, upload-time = "2026-07-08T12:35:16.048Z" }, - { 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" }, + { url = "https://files.pythonhosted.org/packages/15/2b/51e32514a4e9b715375c99721aadff0f24164cc2049b8269eda4de82a814/grpcio-1.83.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:28f6c35ac8fcf10e4594f138e468f194360089dde40d126a7033e863fc479930", size = 6303167, upload-time = "2026-07-23T15:19:33.78Z" }, + { url = "https://files.pythonhosted.org/packages/39/33/b5b50fc2c6fbe350e04814047bb2d409feec7b36ef8b170254c050e06bc0/grpcio-1.83.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:33898e6a28e4ae598f1577cb1c4fec2a15c033d0ec52b9b45a09610dd045b9da", size = 12160538, upload-time = "2026-07-23T15:19:35.958Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5f/734e72e7b9f79bcf0b2c270b8d3bca0e4ebb97a27a50d06240b145f6d41e/grpcio-1.83.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6fb8a1dd0c6f0f931e69e9d0dc6d1c406ed2a44fa963414eafba07b7fb685d16", size = 6869310, upload-time = "2026-07-23T15:19:38.607Z" }, + { url = "https://files.pythonhosted.org/packages/a4/17/a1735f215b2a5cd43c38b79eac072ad197e61be9829905b6b29550abd0db/grpcio-1.83.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2b5e75c34842cd9c1b95285ca395c6a569664b81e3ffa6b714125922942abaaf", size = 7613472, upload-time = "2026-07-23T15:19:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/b2/78/c9e81f806ac704b6b145cb01628db398985b1f8dfdc10e23b55fb0902b3d/grpcio-1.83.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeb339838db07600481ef869507279b75326c75eac6d10f7afa62a0da1d2bcdd", size = 7040616, upload-time = "2026-07-23T15:19:42.349Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ba/94cd5af859876049d340480acbb61a959096c84b567f215534faa78d0424/grpcio-1.83.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f47d62808b4c0a97b78bff88a6d4ca283a2a492b9a04a87d814af95ca3b9c19c", size = 7570491, upload-time = "2026-07-23T15:19:44.357Z" }, + { url = "https://files.pythonhosted.org/packages/3e/15/108d30d5a5c964312ae8b9cb0e8cc5b3c1cc68d8f757cca52b3565534d26/grpcio-1.83.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62003babc444a606dcd1f009cd16391ce23669ae4ad6ec267a873da7937a69f5", size = 8605036, upload-time = "2026-07-23T15:19:46.454Z" }, + { url = "https://files.pythonhosted.org/packages/ea/23/3828ae13c3db8233d123ad612747665817b952d8a954f32390230b582336/grpcio-1.83.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1aa567f8c3f19850ffd5d2858c9a8ea7c80f0db6c01186b71eb31e923ec984f5", size = 7981587, upload-time = "2026-07-23T15:19:48.913Z" }, + { url = "https://files.pythonhosted.org/packages/17/5b/77af31228f55f55a2a5112bb0077ad0a1c4d23dbb0c2853a62475bbdcc14/grpcio-1.83.0-cp312-cp312-win32.whl", hash = "sha256:cb2906c61db4f9c64cc360054b5df70eeb81846228e9e56a4944bd415a63dadc", size = 4394004, upload-time = "2026-07-23T15:19:50.618Z" }, + { url = "https://files.pythonhosted.org/packages/c0/da/f706e39550e7a3732ce2b9c5926107a93d74a802775b19b642a6df27dc96/grpcio-1.83.0-cp312-cp312-win_amd64.whl", hash = "sha256:1c699bbb20f143c8f2bff219de578aa2dc1f919399d67dc702b038b986ee62df", size = 5158525, upload-time = "2026-07-23T15:19:52.246Z" }, ] [[package]] @@ -1284,27 +1276,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.21" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/36/6f65aa9989acdec45d417192d8f4e7921931d8a6cf87ac74bce3eed98a8e/ruff-0.15.21.tar.gz", hash = "sha256:d0cfc841c572283c36548f82664a54ce6565567f1b0d5b4cf2caac693d8b7500", size = 4769401, upload-time = "2026-07-09T20:01:34.005Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/c6/ede15cac6839f3dbce52565c8f5164a8210e669c7bc4decb03e5bdf47d0d/ruff-0.15.21-py3-none-linux_armv6l.whl", hash = "sha256:63ea0e965e5d73c90e95b2434beeafc70820536717f561b32ab6e777cb9bdf5d", size = 10854342, upload-time = "2026-07-09T20:00:53.998Z" }, - { url = "https://files.pythonhosted.org/packages/28/9d/d825b07ee7ea9e2d61df92a860033c94e06e7300d50a1c2653aac27d24fe/ruff-0.15.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0f212c5d7d54c01bbfe6dcab02b724a39300f3e34ed7acbe995ccb320a2c58bd", size = 11139539, upload-time = "2026-07-09T20:00:57.809Z" }, - { url = "https://files.pythonhosted.org/packages/f5/de/3b107712e642f063c7a9e0887c427b22cb44097de5aab36c05f2e280670c/ruff-0.15.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e6312e41bc96791299614995ea3a977c5857c3b5662b1ecef6755b02b87cb646", size = 10595437, upload-time = "2026-07-09T20:01:00.006Z" }, - { url = "https://files.pythonhosted.org/packages/9a/6f/b4523cc90ba239ede441447a19d0c968846a3012e5a0b0c5b62831a3d5e3/ruff-0.15.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01d65b4831c6b2a4ba8ee6faa84049d44d982b7a706e622c4094c509e51673be", size = 10990053, upload-time = "2026-07-09T20:01:02.187Z" }, - { url = "https://files.pythonhosted.org/packages/92/cc/c6a9872a5375f0628875481cf2f66b13d7d865bf3ca2e57f91c7e762d976/ruff-0.15.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c5a913a589120ce67933d5d05fd6ddbcc2481c6a054980ee767f7414c72b4fd", size = 10666096, upload-time = "2026-07-09T20:01:04.299Z" }, - { url = "https://files.pythonhosted.org/packages/ab/97/c621f7a17e097f1790fa3af6374138823b330b2d03fc38337945daca212c/ruff-0.15.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef04b681d02ad4dc9620f00f83ac5c22f652d0e9a9cfe431d219b16ad5ccc41", size = 11537011, upload-time = "2026-07-09T20:01:06.771Z" }, - { url = "https://files.pythonhosted.org/packages/ea/51/d928727e476e25ccc57c6f449ffd80241a651a973ad949d39cfb2a771d28/ruff-0.15.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16d090c0740916594157e75b80d666eab8e78083b39b3b0e1d698f4670a17b86", size = 12347101, upload-time = "2026-07-09T20:01:08.859Z" }, - { url = "https://files.pythonhosted.org/packages/1e/88/8cd62026802b16018ad06931d87997cf795ba2a6239ab659606c87d96bf0/ruff-0.15.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a10e74757dd65004d779b73e2f3c5210156d9980b41224d50d2ebcf1db51e67", size = 11572001, upload-time = "2026-07-09T20:01:11.092Z" }, - { url = "https://files.pythonhosted.org/packages/b2/97/f63084cf55444fc110e8cb985ebfcc592af47f597d44453d778cb81bc156/ruff-0.15.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bab0905d2f29e0d9fbc3c373ed23db0095edaa3f71f1f4f519ec15134d9e85c8", size = 11549239, upload-time = "2026-07-09T20:01:13.27Z" }, - { url = "https://files.pythonhosted.org/packages/9d/77/f107da4a2874b7715914b03f09ba9c54424de3ff8a1cc5d015d3ee2ce0ac/ruff-0.15.21-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:00eca240af5789fec6fe7df74c088cc1f9644ed83027113468efba7c92b94075", size = 11535340, upload-time = "2026-07-09T20:01:15.206Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e9/601deb322d3303a7bf212b0100ead6f2ee3f6a044d89c30f2f92bf83c731/ruff-0.15.21-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262ab31557a75141325e32d3357f3597645a7f084e732b6b054dde428ecd9341", size = 10964048, upload-time = "2026-07-09T20:01:17.723Z" }, - { url = "https://files.pythonhosted.org/packages/ea/2e/0f2176d1e99c15192caea19c8c3a0a955246b4cb4de795042eeb616345cd/ruff-0.15.21-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:659c4e7a4212f83306045ec7c5e5a356d16d9a6ef4ae0c7a4d872914fc655d9d", size = 10667055, upload-time = "2026-07-09T20:01:19.73Z" }, - { url = "https://files.pythonhosted.org/packages/48/60/abd74a02e0c4214f12a68becfd30af7165cfdcb0e661ecdc60bbb949c09a/ruff-0.15.21-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9e866eab611a5f959d36df2d10e446973a3610bc42b0c15b31dc27977d59c233", size = 11242043, upload-time = "2026-07-09T20:01:21.947Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c6/583075d8ccabb4b229345edcaf1545eb3d8d6be90f686a479d7e94088bbf/ruff-0.15.21-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e89bc93c0d3803ba870b55c29671bad9dc6d94bb1eb181b056b52eb05b52854f", size = 11648064, upload-time = "2026-07-09T20:01:24.023Z" }, - { url = "https://files.pythonhosted.org/packages/3a/3c/37d0ecb729a7cc2d393ea7dce316fc585680f35d93b8d62139d7d0a3700c/ruff-0.15.21-py3-none-win32.whl", hash = "sha256:01f8d5be84823c172b389e123174f781f9daf86d6c58719d603f941932195cdd", size = 10896555, upload-time = "2026-07-09T20:01:26.941Z" }, - { url = "https://files.pythonhosted.org/packages/c0/b8/e43466b2a6067ce91e669068f6e28d6c719a920f014b070d5c8731725de3/ruff-0.15.21-py3-none-win_amd64.whl", hash = "sha256:d4b8d9a2f0f12b816b50447f6eccb9f4bb01a6b82c86b50fb3b5354b458dc6d3", size = 12038772, upload-time = "2026-07-09T20:01:29.497Z" }, - { url = "https://files.pythonhosted.org/packages/dd/75/e90ab9aeece218a9fc5a5bc3ec97d0ee6bb3c4ff95869463c1de58e29a1c/ruff-0.15.21-py3-none-win_arm64.whl", hash = "sha256:6e83115d4b9377c1cbc13abf0e051f069fab0ef815ea0504a8a008cee24dd0a8", size = 11375265, upload-time = "2026-07-09T20:01:31.772Z" }, +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" }, + { url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" }, + { url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" }, + { url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" }, + { url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" }, + { url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" }, + { url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" }, + { url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" }, + { url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" }, + { url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" }, ] [[package]] @@ -1318,7 +1310,7 @@ wheels = [ [[package]] name = "semgrep" -version = "1.169.0" +version = "1.171.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -1349,28 +1341,28 @@ dependencies = [ { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/eb/9a41faa086fc40bc0e7839e24252b96cbc4d5c826d13c11525c0b3d6503e/semgrep-1.171.0.tar.gz", hash = "sha256:0e1da44aa535fe5773ba20ec546c8f3c727af052a2244d2e957aa1d360023ada", size = 499332, upload-time = "2026-07-22T23:06:28.268Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/12/cf/1e776b9eda23cba01129d106048b74752c617f8ad80153f3c06e252f3b44/semgrep-1.171.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-macosx_10_14_x86_64.whl", hash = "sha256:861090849a903e28d033ee9586155e9f9cb0a4645303df002379f56634c8e1bc", size = 45303065, upload-time = "2026-07-22T23:08:45.189Z" }, + { url = "https://files.pythonhosted.org/packages/df/42/474d38efe0e677a0b8d1454bf02ff3b912906afb2efb549c2a033927960f/semgrep-1.171.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-macosx_11_0_arm64.whl", hash = "sha256:8683ecbceef6476402a23e1b7b39c6a98c55f11e0389974d160de48a0491e37c", size = 49305173, upload-time = "2026-07-22T23:08:48.986Z" }, + { url = "https://files.pythonhosted.org/packages/ab/99/0e99dff88b293c3841f90c1476f417ea9acf01291a1ad420396da223cf40/semgrep-1.171.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-manylinux_2_34_aarch64.whl", hash = "sha256:a3b08d3581ae4d3fc815fc34452e6d9cac91ca94885a4fce27ec96f92edc0dae", size = 71690152, upload-time = "2026-07-22T23:08:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/79/b9/269d27f2c7d03efaa28ec928d900ee650e581b8b05e7308dbe5d68ba78a7/semgrep-1.171.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-manylinux_2_34_x86_64.whl", hash = "sha256:3f12149e20e512289e517ae4e07e8db6bc3d251c9bdb59c3a59c0c385322893b", size = 69540171, upload-time = "2026-07-22T23:09:00.436Z" }, + { url = "https://files.pythonhosted.org/packages/d2/81/0ca340087e88b378ef9fd06ec4c4bda0ef1069a6988bd159ca0c2bb03d9a/semgrep-1.171.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-musllinux_1_2_aarch64.whl", hash = "sha256:81ecc781bb964a22f92d7053e74b9afe534d7bdafcd3a0e44195872e560e355c", size = 78468596, upload-time = "2026-07-22T23:09:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/a6/67/1c9a8eee4a0ee0702f4e295910ce93c0eae715c62fe505ea1b8c9f2cbc77/semgrep-1.171.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-musllinux_1_2_x86_64.whl", hash = "sha256:c5aa8db65daabfd9644dcb09b4651ab914f5beaa547ec2cb15c02a056a7bc0be", size = 75965791, upload-time = "2026-07-22T23:09:13.797Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f5/4d12d31a8ba42dd0ee30ea7ddf1201095761c15b3c0528e0e1dbb414d143/semgrep-1.171.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-win_amd64.whl", hash = "sha256:69884518e14c4dae62ad1f7d78c14dd4dbd5c44c920962c772f59fba3f69046d", size = 57241691, upload-time = "2026-07-22T23:09:19.173Z" }, ] [[package]] name = "sentry-sdk" -version = "2.66.0" +version = "2.66.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/48/ff/670abe04c5072719b5060ed93851d0d69525d60f8f2c5810f8becd58f9c1/sentry_sdk-2.66.0.tar.gz", hash = "sha256:9727d35aa83c56cd53294676fe65b96296a334c9ce107fa2142bd70f47acb265", size = 935745, upload-time = "2026-07-16T12:42:04.663Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/6f/d59cad0889d15fde85254cf58e701484de3f3f0406003b3197746910b19b/sentry_sdk-2.66.1.tar.gz", hash = "sha256:f882fb08710c5f8bfc603aafa3e901b384009a19cc3f76a572b863392ee81cdc", size = 940543, upload-time = "2026-07-22T12:26:54.553Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/bb/49b10783f29067da2eec179320617e94faf63196609de47aeab3c26c3325/sentry_sdk-2.66.0-py3-none-any.whl", hash = "sha256:096136c214c602be2b323524d30755dc5b30ec5a218a206207f33b12c05c6f11", size = 504769, upload-time = "2026-07-16T12:42:02.919Z" }, + { url = "https://files.pythonhosted.org/packages/89/d3/726bd88f0eece09ddf431bea4c9191c18e7a8d070b854eb0014d447712ee/sentry_sdk-2.66.1-py3-none-any.whl", hash = "sha256:86002793161d9a95ef04bdd8d442e9bfece5d989b755f05d6360215094a7aff6", size = 505555, upload-time = "2026-07-22T12:26:52.71Z" }, ] [[package]] @@ -1499,27 +1491,27 @@ wheels = [ [[package]] name = "ty" -version = "0.0.59" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/b0/84ae7b3bf6e3e9f57eb9635eeff5a80b36e57aa089f40be0fb5c384fa176/ty-0.0.59.tar.gz", hash = "sha256:53e53ffeed78ad59cd237fa8ea1316d2b94e13efdea9a945698acab549e005aa", size = 6145435, upload-time = "2026-07-12T20:22:02.781Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/e8/650b42fbef4d48e6ca682b0b6e9b68fa8fcf55cbb0a6892ab89990018b6f/ty-0.0.59-py3-none-linux_armv6l.whl", hash = "sha256:f8fb08a767ef8f11ea3c537b9d77860726cc2bc39e6f77ad13c02d5b289f20a7", size = 11700328, upload-time = "2026-07-12T20:21:26.046Z" }, - { url = "https://files.pythonhosted.org/packages/22/ac/0ca3a89d5f59ae5f308e5e83428cac5f9143200767743e052fba90b4b81e/ty-0.0.59-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c7f4d5630836c8a0ba13dd4ac7bdae080a7d6ebe965b817ff642dc961bcf2a53", size = 11494310, upload-time = "2026-07-12T20:21:28.491Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f8/5076de6001cefbccd8e6dc8472262697e43308ff66b0e87c72abba136357/ty-0.0.59-py3-none-macosx_11_0_arm64.whl", hash = "sha256:872f6fb02c6db5553c4d5fb283b3d50f0985fb9a29a910e4fda4793a775c1926", size = 11026797, upload-time = "2026-07-12T20:21:30.879Z" }, - { url = "https://files.pythonhosted.org/packages/2e/0f/fca28481b6a138e2b798ad9fdc98a095475f9104948ba242fce4b477782b/ty-0.0.59-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2af8eefbfe806337770eec12c0c819c5f1b8f5b85f8369cb1cc9fa25234a2208", size = 11475304, upload-time = "2026-07-12T20:21:33.041Z" }, - { url = "https://files.pythonhosted.org/packages/08/4b/1fed8b81b389ef4bbc0400f19e05fc16496b162577779dc0e5fc65ac216c/ty-0.0.59-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0acf8b76a1c9a7ddef460b42475f6c76193164426ab080783af1c3175b4b999b", size = 11533131, upload-time = "2026-07-12T20:21:35.189Z" }, - { url = "https://files.pythonhosted.org/packages/5f/fc/04eec35e05a10e0fea1c6503a290ccc3935efda9c845aff64e83282c1af7/ty-0.0.59-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:043c2e00eb1d7475f928af7dedd71f69b64e69bfca55e36f4c968479e1373fc4", size = 12205932, upload-time = "2026-07-12T20:21:37.324Z" }, - { url = "https://files.pythonhosted.org/packages/a9/dd/a61de859659fa11b55917ad38340a8f2c61f5ae17d1874929f29084c6990/ty-0.0.59-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0d688d857441df57f48fca66c029d85cf737c510e7be1d01144cdad1e58d968", size = 12758406, upload-time = "2026-07-12T20:21:39.525Z" }, - { url = "https://files.pythonhosted.org/packages/c6/e8/fa66f05997eab8ca75fc4f17320140e25467849e0cc75597f898cc22099c/ty-0.0.59-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a96c9f88394a3b42c737e2125b2330543f0d90a43b49761f377d96f8c3ee0d62", size = 12288176, upload-time = "2026-07-12T20:21:41.784Z" }, - { url = "https://files.pythonhosted.org/packages/15/68/0fca59963bd5123f42d5f7da50667e7a52e8e9615e3a16d8c2c0d3b2d143/ty-0.0.59-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f08dbcb268edcafcb152e59475b5b495ce28d0b340a395c09943557678f4d5a6", size = 12028471, upload-time = "2026-07-12T20:21:43.82Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5b/cd7dabbbab392578f11179919da5c25d8c3322e5388a688f539ea0539603/ty-0.0.59-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8812764b9a40fdc98df1272826e73a298ef56b06681135e643bcf90aad1896f7", size = 12297646, upload-time = "2026-07-12T20:21:45.76Z" }, - { url = "https://files.pythonhosted.org/packages/1d/37/2e9c94f0b383d8cbe1a35517ab470b7810bc9d7501603ab532bcd5be5e90/ty-0.0.59-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fd53b8581641d8dad7bfac6d5ea589e91a883d6837e0b9a286fdae30722b7c69", size = 11432519, upload-time = "2026-07-12T20:21:47.694Z" }, - { url = "https://files.pythonhosted.org/packages/9d/0a/af93e9785200f11ac416cc20235fc2464c9bd978e791190684ea0e458795/ty-0.0.59-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:86da5872124a41877d95058bc17d33ddcff034b587eb5f1e2917ab88ba227dac", size = 11554993, upload-time = "2026-07-12T20:21:49.671Z" }, - { url = "https://files.pythonhosted.org/packages/4b/dd/651bf87e20d00376c81b19124756491cffaf20eb8bec05a8794e5a8cf641/ty-0.0.59-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a233eef5f2fd4d894881e4a0aec83c9f172bfae1d787d6596ee1939fcc7723e", size = 11818230, upload-time = "2026-07-12T20:21:51.659Z" }, - { url = "https://files.pythonhosted.org/packages/16/50/c947c4155fea751d135b19affdf734bbce72a94e446b866cf0c62f8bed69/ty-0.0.59-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7ff678c18b5f1e3128b75a35e50dee7908dea55155baa31cd790619d5014cbf5", size = 12135194, upload-time = "2026-07-12T20:21:53.796Z" }, - { url = "https://files.pythonhosted.org/packages/b1/13/e5feb138888de1e95037c843571bbbd4ac21bf0a190507468098599a321f/ty-0.0.59-py3-none-win32.whl", hash = "sha256:cf8abb4b8095c5fe39102b8127f5886db308c8d4600909ddbc905512ce9c8163", size = 11179249, upload-time = "2026-07-12T20:21:55.752Z" }, - { url = "https://files.pythonhosted.org/packages/76/dd/52914dcbeeba92c207de40ef7109a58dcb5527aeb21c8f8feb7402aa9e29/ty-0.0.59-py3-none-win_amd64.whl", hash = "sha256:1dde20a82243d24407869e5a608c2f15efddd5cefc662aef461a5af84bfb3f8b", size = 12251079, upload-time = "2026-07-12T20:21:58.1Z" }, - { url = "https://files.pythonhosted.org/packages/d4/8f/ac36fde77e223297454c1e0aeb8888c169eaacf3163bb609e3af942c88cb/ty-0.0.59-py3-none-win_arm64.whl", hash = "sha256:987043ee9e021f49493d9135891ac69c1affeee0d4ad4480c5fa4d9c975fc91b", size = 11650921, upload-time = "2026-07-12T20:22:00.348Z" }, +version = "0.0.63" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/ce/cbeaa5c7576fec643609dfbf200d59493523b1cc0481d4e7a5effcbf0630/ty-0.0.63.tar.gz", hash = "sha256:c2f66439393b3acac69306c117d4ae44638ce5fffa4a20c21046e85bd473359f", size = 6280695, upload-time = "2026-07-23T11:41:39.845Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/e4/d17a8e113ab15c692fe6fb9c422112d4bee7e829d80ced35826b45d98d98/ty-0.0.63-py3-none-linux_armv6l.whl", hash = "sha256:9a4ef7782e3af314fb63d006bec5ac3025bd70fee774b4dcfb5e44e8564f1994", size = 12056302, upload-time = "2026-07-23T11:41:03.5Z" }, + { url = "https://files.pythonhosted.org/packages/be/0b/357234c815dc4bfcc88c3f860aa0983fe4228c8aa2a5bb16c35ee08a94af/ty-0.0.63-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:01eab0ab70d51ad10298aa2d4b058b387a1fe93e5ee52d2a1ee23e9c69ba8354", size = 11737674, upload-time = "2026-07-23T11:41:05.862Z" }, + { url = "https://files.pythonhosted.org/packages/1c/13/193d9aeeb6774690351cff9fafabd3ae9b54cc225d125e07fa004ce23bdc/ty-0.0.63-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a671b61eaad16178389e05b9c108c9cb75ae8d84968fe55906484f55d6268338", size = 11264191, upload-time = "2026-07-23T11:41:07.963Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/c5843e16759a2b25fc8a7197473474038e585ac9fdaa6fa16aeb6384bf6a/ty-0.0.63-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b255cc83d95c51bb9ee4931303fdbece8cb1d6d7c655eb77a3f2c8f349fb64d6", size = 11818890, upload-time = "2026-07-23T11:41:09.885Z" }, + { url = "https://files.pythonhosted.org/packages/06/20/adf83ae1fc570d9bb449605e1eeeaa523ad2329ca838a636698b5c135d11/ty-0.0.63-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0da1e8183aa4f893421a173904478021498f542ee41273660538da6649a9631c", size = 11853141, upload-time = "2026-07-23T11:41:12.151Z" }, + { url = "https://files.pythonhosted.org/packages/ea/90/f8effd846e3ee13486ea08257c13094d58b9f188c5641bf609d6a7d5c09f/ty-0.0.63-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:128f38eb5a67199e3811426386f7ec96f41251ddf45e04ddc0bcbb29d4853245", size = 12545184, upload-time = "2026-07-23T11:41:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/b8/76/aac3a30d40431eb1d329acea016b248f5b6255e30ef3d28e19447e344be2/ty-0.0.63-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bedf34aff8b0557f2a7119b314a68a9c9e58c1d21554d0e2748f2c5fe6a1f638", size = 13062375, upload-time = "2026-07-23T11:41:16.441Z" }, + { url = "https://files.pythonhosted.org/packages/62/3d/0158733932893e17f6008dadd74c8d7aed422fefc66e47fe77888014fe8c/ty-0.0.63-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7328d63c34587606dce02935a25404f54cd0161bfe413b8f7fc21d174aead612", size = 12619461, upload-time = "2026-07-23T11:41:18.538Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/e280ad095b050778f16f493d49a073fa5f6d8f301d3e2e59be6a672ba05c/ty-0.0.63-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504c4457f3a62afe836c1f26a2c9a12549299095f9cc4146778558df51a7515c", size = 12376402, upload-time = "2026-07-23T11:41:20.482Z" }, + { url = "https://files.pythonhosted.org/packages/57/57/619788bf335cb86b090470b557ae1eed33613dc24e12142505d32b462e58/ty-0.0.63-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7394ed39424b027c89d5f0c818871c791e33958b88f5bb13e14bd4a12a3ca631", size = 12671377, upload-time = "2026-07-23T11:41:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/07/a2/0aff2cdd3c98e2c4729337542b8da0ce1980790e1600e0c4a717a1f46293/ty-0.0.63-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:94c3f155230490f8fb505911655e940c630c25cc0c35a76690cb413254fb4437", size = 11768090, upload-time = "2026-07-23T11:41:24.558Z" }, + { url = "https://files.pythonhosted.org/packages/43/4a/cdb5f1d26154144dfee08e1bd671daf827238170f7cee9dad43990bb2016/ty-0.0.63-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c16e1d8b4f0ae99106d5f13a894034a202baf6cdab61e2bb1a239de82904f839", size = 11867747, upload-time = "2026-07-23T11:41:26.794Z" }, + { url = "https://files.pythonhosted.org/packages/7e/26/ecc09ecb70bc9fbfdf42fa57ff29568f173e8200df575a0d72dc2b8486f9/ty-0.0.63-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b66293dad89eaed4b9fbf3b661614dbeb85b59ba037178a3f9a0adedf264da5c", size = 12120000, upload-time = "2026-07-23T11:41:28.731Z" }, + { url = "https://files.pythonhosted.org/packages/14/07/862822f9c2c397785b69b25cf79b4dfc3c0d55684b9adf11ac194450f8e1/ty-0.0.63-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8b29cc832e2ec73502c97dd7325d6ae0e085b34a33529a0f785192317d9862fc", size = 12477862, upload-time = "2026-07-23T11:41:30.847Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c2/50b08b641578d6f48e8aa28a91d0de32c91aa24dd926ed199e8afd2d37ea/ty-0.0.63-py3-none-win32.whl", hash = "sha256:f0a8fbfd1f990c0c5d85cce018d438969ac79e49247e2192c9745394bb7d17ab", size = 11433155, upload-time = "2026-07-23T11:41:33.28Z" }, + { url = "https://files.pythonhosted.org/packages/92/2d/d422a5568f0d1f317186be22241288dc6e08b71dc24aca223f22038ac2d2/ty-0.0.63-py3-none-win_amd64.whl", hash = "sha256:2aa2370bdd6f42e9f37518c812379bef70b9162f9e5aad511495229b0b71cb93", size = 12450036, upload-time = "2026-07-23T11:41:35.559Z" }, + { url = "https://files.pythonhosted.org/packages/35/97/2c9748e28ead0650c7ad3e5f74f178832ceabd7cb5c272a882f29eb32ee4/ty-0.0.63-py3-none-win_arm64.whl", hash = "sha256:95ac1a62162c3c7ac204731e95ebf766d62a46a7bfa238a6acbe923fcb772cb1", size = 11826243, upload-time = "2026-07-23T11:41:37.749Z" }, ] [[package]] From a123ab88ceb0a07f3d7165a30f718285e33c3f17 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Fri, 24 Jul 2026 19:33:18 -0700 Subject: [PATCH 4/5] fix(ci): resolve pkgx certificate bundles before vcpkg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve pkgx’s concrete CA path before vcpkg bootstrap so Unix CI and coverage jobs do not pass an unexpanded fallback expression to curl. Extend the pkgx-first Semgrep policy to reject non-Windows uv toolchain installations while preserving the approved Windows wheel fallback. --- .github/workflows/ci.yml | 14 ++++++++++++++ .github/workflows/codecov-upload.yml | 13 +++++++++++++ semgrep.yaml | 19 ++++++++++++++++++- tests/semgrep/tooling_policy.yml | 6 ++++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6395d8565..e9513e6c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,6 +92,20 @@ jobs: cmake.org@${{ steps.tool-versions.outputs.cmake }} ninja-build.org@${{ steps.tool-versions.outputs.ninja }} + - name: Resolve pkgx certificate bundle + if: runner.os != 'Windows' + shell: bash + run: | + ssl_cert_file="$( + env -u SSL_CERT_FILE pkgx +curl.se/ca-certs env | + sed -n 's/^SSL_CERT_FILE=//p' + )" + if [[ -z "$ssl_cert_file" || ! -r "$ssl_cert_file" ]]; then + echo "pkgx CA bundle is missing or unreadable: $ssl_cert_file" >&2 + exit 1 + fi + echo "SSL_CERT_FILE=$ssl_cert_file" >> "$GITHUB_ENV" + - name: Set up Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: diff --git a/.github/workflows/codecov-upload.yml b/.github/workflows/codecov-upload.yml index fdef9b81c..7875a3bbe 100644 --- a/.github/workflows/codecov-upload.yml +++ b/.github/workflows/codecov-upload.yml @@ -70,6 +70,19 @@ jobs: cmake.org@${{ steps.tool-versions.outputs.cmake }} ninja-build.org@${{ steps.tool-versions.outputs.ninja }} + - name: Resolve pkgx certificate bundle + shell: bash + run: | + ssl_cert_file="$( + env -u SSL_CERT_FILE pkgx +curl.se/ca-certs env | + sed -n 's/^SSL_CERT_FILE=//p' + )" + if [[ -z "$ssl_cert_file" || ! -r "$ssl_cert_file" ]]; then + echo "pkgx CA bundle is missing or unreadable: $ssl_cert_file" >&2 + exit 1 + fi + echo "SSL_CERT_FILE=$ssl_cert_file" >> "$GITHUB_ENV" + - name: Report coverage toolchain run: | cmake --version diff --git a/semgrep.yaml b/semgrep.yaml index 45598b758..f53280023 100644 --- a/semgrep.yaml +++ b/semgrep.yaml @@ -3,7 +3,7 @@ rules: - id: cdt.tooling.pkgx-first-toolchain languages: - - generic + - yaml severity: ERROR message: >- Keep CMake and Ninja setup pkgx-first. Use pkgxdev/setup with versions @@ -25,6 +25,23 @@ rules: (?mi)^[\t ]*(?:-[\t ]*)?uses:[\t ]*(?:lukka/get-cmake|jwlawson/actions-setup-cmake|aminya/setup-cpp)@ - pattern-regex: |- (?mi)^[^\r\n]*(?:apt-get|brew|choco|winget|pipx?)[\t ]+install[^\r\n]*\b(?:cmake|ninja)\b + - patterns: + - pattern: | + run: $COMMAND + - metavariable-regex: + metavariable: $COMMAND + regex: |- + (?mi)\buv(?:[\t ]+[^\s]+)*[\t ]+tool[\t ]+install[^\r\n]*\b(?:cmake|ninja)\b + - pattern-not-inside: | + - ... + if: runner.os == 'Windows' + ... + run: $COMMAND + - pattern-not-inside: | + $JOB: + ... + runs-on: windows-latest + ... - id: cdt.cpp.no-crtp-inheritance languages: - cpp diff --git a/tests/semgrep/tooling_policy.yml b/tests/semgrep/tooling_policy.yml index d5b5d9b5c..f3c92d877 100644 --- a/tests/semgrep/tooling_policy.yml +++ b/tests/semgrep/tooling_policy.yml @@ -13,6 +13,12 @@ jobs: # ruleid: cdt.tooling.pkgx-first-toolchain - run: brew install cmake ninja + non-windows-uv-tool-install: + runs-on: ubuntu-latest + steps: + # ruleid: cdt.tooling.pkgx-first-toolchain + - run: uv tool install --no-build "ninja==1.13.0" + pkgx-first: runs-on: ubuntu-latest steps: From 56f13cdcf5bd602e59921a3c3af635f611899d4f Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Fri, 24 Jul 2026 20:56:51 -0700 Subject: [PATCH 5/5] fix(ci): restore CodeQL extraction and bound parallel builds - Disable unused C++ module scanning so CodeQL can observe compiler calls. - Run the CGAL/oneTBB contract only on pinned Ubuntu toolchains while retaining reference coverage across all platforms. --- .github/CONTRIBUTING.md | 10 +++++----- .github/workflows/ci.yml | 5 +++++ CMakeLists.txt | 3 +++ README.md | 4 ++-- docs/multithreading.md | 6 +++--- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 66422613a..9f74a05fc 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -54,11 +54,11 @@ substantial implementation so its maintenance value and scope can be agreed upon CGAL/oneTBB configuration and runs the same scientific suite plus the replayable parallel stress launcher, for 25 entries. When changing C++ behavior, also run `just clang-tidy` with the pinned LLVM 22 toolchain and review its advisory diagnostics. - GitHub Actions runs both `just ci` and `just build-parallel` 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. Toolchain setup is pkgx-first; because pkgx does not currently publish its CMake and Ninja - packages for Windows, that job uses the exact Justfile pins available as PyPI wheels through - `uv tool install --no-build`. + GitHub Actions runs `just ci` in its Ubuntu GCC, Ubuntu Clang, macOS AppleClang, and Windows MSVC jobs. The two + Ubuntu jobs also run `just build-parallel` to exercise the opt-in CGAL/oneTBB contract. The Windows job continues + to compile with native MSVC; LLVM tooling is used only for source formatting. Toolchain setup is pkgx-first; + because pkgx does not currently publish its CMake and Ninja packages for Windows, that job uses the exact Justfile + pins available as PyPI wheels through `uv tool install --no-build`. 6. Run the relevant Linux sanitizer configuration for changes involving memory, lifetime, undefined behavior, or concurrency: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9513e6c6..a3d753e18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,21 +31,25 @@ jobs: compiler_package: gnu.org/gcc@16 cc: gcc cxx: g++ + run_parallel_contract: true - name: Ubuntu Clang os: ubuntu-latest compiler_package: llvm.org@22 cc: clang cxx: clang++ + run_parallel_contract: true - name: macOS AppleClang os: macos-latest compiler_package: "" cc: clang cxx: clang++ + run_parallel_contract: false - name: Windows MSVC os: windows-latest compiler_package: "" cc: cl cxx: cl + run_parallel_contract: false steps: - name: Disable Git autocrlf on Windows @@ -157,6 +161,7 @@ jobs: run: just ci - name: Run the opt-in CGAL/oneTBB contract + if: matrix.run_parallel_contract env: CC: ${{ matrix.cc }} CDT_PKGX_COMPILER_PACKAGE: ${{ matrix.compiler_package }} diff --git a/CMakeLists.txt b/CMakeLists.txt index a02f1e893..db70ee43c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,6 +26,9 @@ project( DESCRIPTION "Fast Causal Dynamical Triangulations in C++" LANGUAGES CXX) +# CDT++ does not use C++ modules, so avoid unnecessary dependency scanning. +set(CMAKE_CXX_SCAN_FOR_MODULES OFF) + # 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 "-rc2") diff --git a/README.md b/README.md index 535b8d3b3..e945e31fb 100644 --- a/README.md +++ b/README.md @@ -237,8 +237,8 @@ just python-fix # Apply safe Ruff fixes and formatting 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 `just ci` followed by -`just build-parallel`. Windows continues to compile with native MSVC; the +AppleClang, and Windows MSVC jobs all run `just ci`; the two Ubuntu jobs also +run `just build-parallel`. 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 diff --git a/docs/multithreading.md b/docs/multithreading.md index 4bcb311d4..a9f47633e 100644 --- a/docs/multithreading.md +++ b/docs/multithreading.md @@ -32,9 +32,9 @@ ctest --preset parallel-smoke The supported release matrix is the same C++23 matrix declared by the root CMake configuration: Linux with GCC 13.3 or newer or Clang 22 or newer, macOS -with AppleClang 15 or newer, and Windows with MSVC 19.34 or newer. The primary -CI workflow runs `build-parallel` with Ubuntu GCC, Ubuntu Clang, macOS -AppleClang, and Windows MSVC. The Linux AddressSanitizer workflow supplies the +with AppleClang 15 or newer, and Windows with MSVC 19.34 or newer. Every primary +CI job runs the reference contract; the Ubuntu GCC and Ubuntu Clang jobs also +run `build-parallel`. The Linux AddressSanitizer workflow supplies the additional parallel sanitizer cell. Configuration fails when the selected CGAL package does not provide `CGAL::TBB_support`; a compiler version alone is not evidence that the dependency boundary is available.